├── .gitignore ├── .travis.yml ├── README.md ├── lib └── oop.ex ├── logo ├── logo.png ├── logo.svg └── logotype_horizontal.png ├── mix.exs └── test ├── oop_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | sudo: false 3 | elixir: 4 | - 1.3.4 5 | otp_release: 6 | - 18.2 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |


oop
2 | 3 | # OOP 4 | 5 | [![Build Status](https://travis-ci.org/wojtekmach/oop.svg?branch=master)](https://travis-ci.org/wojtekmach/oop) 6 | 7 | Are you tired of all of that modules, processes and functions nonsense? Do you want to just use classes, objects and methods? If so, use OOP [1] library in Elixir [2]! 8 | 9 | ## Demo 10 | 11 | [![Lightning Talks - Wojtek Mach (ElixirConfEU 2016)](https://img.youtube.com/vi/5EtV2JUU0Z4/0.jpg)](https://www.youtube.com/watch?v=5EtV2JUU0Z4) 12 | 13 | ## Example 14 | 15 | ```elixir 16 | import OOP 17 | 18 | class Person do 19 | var :name 20 | 21 | def say_hello_to(who) do 22 | what = "Hello #{who.name}" 23 | IO.puts("#{this.name}: #{what}") 24 | end 25 | end 26 | 27 | joe = Person.new(name: "Joe") 28 | mike = Person.new(name: "Mike") 29 | robert = Person.new(name: "Robert") 30 | 31 | joe.say_hello_to(mike) # Joe: Hello Mike 32 | mike.say_hello_to(joe) # Mike: Hello Joe 33 | mike.say_hello_to(robert) # Mike: Hello Robert 34 | robert.say_hello_to(mike) # Robert: Hello Mike 35 | 36 | joe.set_name("Hipster Joe") 37 | joe.name # => Hipster Joe 38 | ``` 39 | 40 | An OOP library wouldn't be complete without inheritance: 41 | 42 | ```elixir 43 | class Animal do 44 | var :name 45 | end 46 | 47 | class Dog < Animal do 48 | var :breed 49 | end 50 | 51 | snuffles = Dog.new(name: "Snuffles", breed: "Shih Tzu") 52 | snuffles.name # => "Snuffles" 53 | snuffles.breed # => "Shih Tzu" 54 | ``` 55 | 56 | ... or multiple inheritance: 57 | 58 | ```elixir 59 | class Human do 60 | var :name 61 | end 62 | 63 | class Horse do 64 | var :horseshoes_on? 65 | end 66 | 67 | class Centaur < [Human, Horse] do 68 | end 69 | 70 | john = Centaur.new(name: "John", horseshoes_on?: true) 71 | john.name # => "John" 72 | john.horseshoes_on? # => true 73 | ``` 74 | 75 | See more usage in the [test suite](test/oop_test.exs). 76 | 77 | ## Installation 78 | 79 | Add `oop` to your list of dependencies in `mix.exs`: 80 | 81 | ```elixir 82 | def deps do 83 | [{:oop, "~> 0.1.0"}] 84 | end 85 | ``` 86 | 87 | [1] According to Alan Kay, the inventor of OOP, "objects" is the lesser idea; the big idea is "messaging". In that sense, I can't agree more with Joe Armstrong's quote that Erlang is "possibly the only object-oriented language". 88 | 89 | [2] Please don't. You've been warned. 90 | 91 | ## License 92 | 93 | The MIT License (MIT) 94 | 95 | Copyright (c) 2015 Wojciech Mach 96 | 97 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 98 | 99 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 100 | 101 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 102 | -------------------------------------------------------------------------------- /lib/oop.ex: -------------------------------------------------------------------------------- 1 | defmodule OOP.Registry do 2 | def start_link do 3 | Agent.start_link(fn -> %{} end, name: __MODULE__) 4 | end 5 | 6 | def register(pid, class) do 7 | Agent.update(__MODULE__, &Map.put(&1, pid, class)) 8 | end 9 | 10 | def get(pid) do 11 | Agent.get(__MODULE__, &Map.get(&1, pid, nil)) 12 | end 13 | end 14 | 15 | defmodule OOP.Application do 16 | use Application 17 | 18 | def start(_type, _args) do 19 | OOP.Registry.start_link() 20 | end 21 | end 22 | 23 | defmodule OOP.Builder do 24 | def create_class(class, superclasses, block, opts) do 25 | quote do 26 | defmodule unquote(class) do 27 | OOP.Builder.ensure_can_be_subclassed(unquote(superclasses)) 28 | 29 | @final Keyword.get(unquote(opts), :final, false) 30 | 31 | def __final__? do 32 | @final 33 | end 34 | 35 | def new(data \\ [], descendant? \\ false) do 36 | OOP.Builder.ensure_can_be_instantiated(unquote(class), descendant?, unquote(opts)) 37 | 38 | 39 | object = :"#{unquote(class)}#{:erlang.unique_integer()}" 40 | 41 | defmodule object do 42 | use GenServer 43 | 44 | def start_link(data) do 45 | GenServer.start_link(__MODULE__, data, name: __MODULE__) 46 | end 47 | 48 | def class do 49 | unquote(class) 50 | end 51 | 52 | def methods do 53 | built_ins = [ 54 | code_change: 3, handle_call: 3, handle_cast: 2, handle_info: 2, 55 | init: 1, start_link: 1, terminate: 2, 56 | class: 0, methods: 0, 57 | ] 58 | 59 | __MODULE__.__info__(:functions) -- built_ins 60 | end 61 | 62 | import Kernel, except: [def: 2] 63 | 64 | Module.register_attribute(__MODULE__, :friends, accumulate: true) 65 | 66 | unquote(block) 67 | 68 | Enum.each(unquote(superclasses), fn superclass -> 69 | parent = superclass.new(data, true) 70 | 71 | for {method, arity} <- parent.methods do 72 | Code.eval_quoted(OOP.Builder.inherit_method(method, arity, parent), [], __ENV__) 73 | end 74 | end) 75 | end 76 | 77 | {:ok, pid} = object.start_link(Enum.into(data, %{})) 78 | OOP.Registry.register(pid, unquote(class)) 79 | 80 | object 81 | end 82 | end 83 | end 84 | end 85 | 86 | def ensure_can_be_subclassed(superclasses) do 87 | Enum.each(superclasses, fn s -> 88 | if s.__final__?, do: raise "cannot subclass final class #{s}" 89 | end) 90 | end 91 | 92 | def ensure_can_be_instantiated(class, descendant?, opts) do 93 | abstract? = Keyword.get(opts, :abstract, false) 94 | 95 | if !descendant? and abstract? do 96 | raise "cannot instantiate abstract class #{class}" 97 | end 98 | end 99 | 100 | def create_method(call, expr) do 101 | # HACK: this is a really gross way of checking if the function is using `this`. 102 | # if so, we let it leak: `var!(this) = data`. 103 | # We do this so that we don't get the "unused variable this" warning when 104 | # we don't use `this`. 105 | using_this? = String.match?(Macro.to_string(expr), ~r"\bthis\.") 106 | 107 | {method, args} = Macro.decompose_call(call) 108 | 109 | handle_call_quoted = 110 | quote do 111 | try do 112 | [do: value] = unquote(expr) 113 | {:reply, {:ok, value}, data} 114 | rescue 115 | e in [RuntimeError] -> 116 | {:reply, {:error, e}, data} 117 | end 118 | end 119 | 120 | quote do 121 | def unquote(call) do 122 | case GenServer.call(__MODULE__, {:call, unquote(method), unquote(args)}) do 123 | {:ok, value} -> value 124 | {:error, e} -> raise e 125 | end 126 | end 127 | 128 | if unquote(using_this?) do 129 | def handle_call({:call, unquote(method), unquote(args)}, _from, data) do 130 | var!(this) = data 131 | unquote(handle_call_quoted) 132 | end 133 | else 134 | def handle_call({:call, unquote(method), unquote(args)}, _from, data) do 135 | unquote(handle_call_quoted) 136 | end 137 | end 138 | end 139 | end 140 | 141 | def inherit_method(method, arity, parent) do 142 | args = (0..arity) |> Enum.drop(1) |> Enum.map(fn i -> {:"arg#{i}", [], OOP} end) 143 | 144 | {:defdelegate, [context: OOP, import: Kernel], 145 | [{method, [], args}, [to: parent]]} 146 | end 147 | 148 | def create_var(field, opts) do 149 | private? = Keyword.get(opts, :private, false) 150 | 151 | quote do 152 | def unquote(field)() do 153 | case GenServer.call(__MODULE__, {:get, unquote(field)}) do 154 | {:ok, value} -> value 155 | {:error, :private} -> raise "Cannot access private var #{unquote(field)}" 156 | end 157 | end 158 | 159 | def unquote(:"set_#{field}")(value) do 160 | GenServer.call(__MODULE__, {:set, unquote(field), value}) 161 | end 162 | 163 | def handle_call({:get, unquote(field)}, {pid, _ref}, data) do 164 | classes = [class() | @friends] 165 | if unquote(private?) and ! OOP.Registry.get(pid) in classes do 166 | {:reply, {:error, :private}, data} 167 | else 168 | {:reply, {:ok, Map.get(data, unquote(field))}, data} 169 | end 170 | end 171 | 172 | def handle_call({:set, unquote(field), value}, _from, data) do 173 | {:reply, value, Map.put(data, unquote(field), value)} 174 | end 175 | end 176 | end 177 | end 178 | 179 | defmodule OOP do 180 | defmacro class(class_expr, block, opts \\ []) do 181 | {class, superclasses} = 182 | case class_expr do 183 | {:<, _, [class, superclasses]} when is_list(superclasses) -> 184 | {class, superclasses} 185 | 186 | {:<, _, [class, superclass]} -> 187 | {class, [superclass]} 188 | 189 | class -> 190 | {class, []} 191 | end 192 | 193 | OOP.Builder.create_class(class, superclasses, block, opts) 194 | end 195 | 196 | defmacro abstract(class_expr, block) do 197 | {:class, _, [class]} = class_expr 198 | 199 | quote do 200 | OOP.class(unquote(class), unquote(block), abstract: true) 201 | end 202 | end 203 | 204 | defmacro final(class_expr, block) do 205 | {:class, _, [class]} = class_expr 206 | 207 | quote do 208 | OOP.class(unquote(class), unquote(block), final: true) 209 | end 210 | end 211 | 212 | defmacro def(call, expr \\ nil) do 213 | OOP.Builder.create_method(call, expr) 214 | end 215 | 216 | defmacro var(field, opts \\ []) do 217 | OOP.Builder.create_var(field, opts) 218 | end 219 | 220 | defmacro private_var(field) do 221 | quote do 222 | var(unquote(field), private: true) 223 | end 224 | end 225 | 226 | defmacro friend(class) do 227 | quote do 228 | @friends unquote(class) 229 | end 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtekmach/oop/b1ce8bf1dff77fc2b34322fcfa287946bd3ac58e/logo/logo.png -------------------------------------------------------------------------------- /logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 13 | 21 | 25 | 41 | 42 | -------------------------------------------------------------------------------- /logo/logotype_horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wojtekmach/oop/b1ce8bf1dff77fc2b34322fcfa287946bd3ac58e/logo/logotype_horizontal.png -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule OOP.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :oop, 6 | version: "0.1.1", 7 | description: "OOP in Elixir!", 8 | package: package(), 9 | elixir: "~> 1.0", 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | deps: deps()] 13 | end 14 | 15 | def application do 16 | [mod: {OOP.Application, []}] 17 | end 18 | 19 | defp deps do 20 | [] 21 | end 22 | 23 | defp package do 24 | [ 25 | maintainers: ["Wojtek Mach"], 26 | licenses: ["MIT"], 27 | links: %{"GitHub" => "https://github.com/wojtekmach/oop"}, 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/oop_test.exs: -------------------------------------------------------------------------------- 1 | defmodule OOPTest do 2 | use ExUnit.Case 3 | import OOP 4 | 5 | test "define empty class" do 6 | c = class Person do 7 | end 8 | 9 | assert c 10 | purge Person 11 | end 12 | 13 | test "instantiate empty object" do 14 | class Person do 15 | end 16 | 17 | alice = Person.new 18 | assert alice.class == Person 19 | purge Person 20 | end 21 | 22 | test "define methods on objects" do 23 | class Person do 24 | def zero do 25 | 0 26 | end 27 | 28 | def sum1(a) do 29 | a 30 | end 31 | 32 | def sum2(a, b) do 33 | a + b 34 | end 35 | end 36 | 37 | alice = Person.new 38 | assert alice.zero() == 0 39 | assert alice.sum1(1) == 1 40 | assert alice.sum2(1, 2) == 3 41 | purge Person 42 | end 43 | 44 | test "define fields" do 45 | class Person do 46 | var :name 47 | 48 | def title(prefix) do 49 | "#{prefix} #{this.name}" 50 | end 51 | end 52 | 53 | alice = Person.new 54 | assert alice.name == nil 55 | 56 | bob = Person.new(name: "Bob") 57 | assert bob.name == "Bob" 58 | bob.set_name("Hipster Bob") 59 | assert bob.name == "Hipster Bob" 60 | assert bob.title("Mr.") == "Mr. Hipster Bob" 61 | 62 | assert alice.name == nil 63 | 64 | purge Person 65 | end 66 | 67 | test "define private fields" do 68 | class AppleInc do 69 | private_var :registered_devices 70 | 71 | def registered_devices_count do 72 | length(this.registered_devices) 73 | end 74 | end 75 | 76 | apple = AppleInc.new(registered_devices: ["Alice's iPhone", "Bob's iPhone"]) 77 | 78 | assert_raise RuntimeError, "Cannot access private var registered_devices", fn -> 79 | apple.registered_devices 80 | end 81 | 82 | assert apple.registered_devices_count == 2 83 | 84 | purge AppleInc 85 | end 86 | 87 | test "define friend class" do 88 | class NSA do 89 | def get_data(company) do 90 | company.registered_devices 91 | end 92 | end 93 | 94 | class Thief do 95 | def get_data(company) do 96 | company.registered_devices 97 | end 98 | end 99 | 100 | class AppleInc do 101 | friend NSA 102 | private_var :registered_devices 103 | end 104 | 105 | apple = AppleInc.new(registered_devices: ["Alice's iPhone", "Bob's iPhone"]) 106 | thief = Thief.new 107 | nsa = NSA.new 108 | 109 | assert_raise RuntimeError, "Cannot access private var registered_devices", fn -> 110 | thief.get_data(apple) 111 | end 112 | 113 | assert nsa.get_data(apple) == ["Alice's iPhone", "Bob's iPhone"] 114 | 115 | purge [AppleInc, Thief, NSA] 116 | end 117 | 118 | test "inheritance" do 119 | class Animal do 120 | var :name 121 | 122 | def title(prefix) do 123 | "#{prefix} #{this.name}" 124 | end 125 | end 126 | 127 | class Dog < Animal do 128 | var :breed 129 | end 130 | 131 | snuffles = Dog.new(name: "Snuffles", breed: "Shih Tzu") 132 | assert snuffles.name == "Snuffles" 133 | assert snuffles.breed == "Shih Tzu" 134 | assert snuffles.title("Mr.") == "Mr. Snuffles" 135 | 136 | purge [Animal, Dog] 137 | end 138 | 139 | test "multiple inheritance" do 140 | class Human do 141 | var :name 142 | end 143 | 144 | class Horse do 145 | var :horseshoes_on? 146 | end 147 | 148 | class Centaur < [Human, Horse] do 149 | end 150 | 151 | john = Centaur.new(name: "John", horseshoes_on?: true) 152 | assert john.name == "John" 153 | assert john.horseshoes_on? == true 154 | 155 | purge [Human, Horse, Centaur] 156 | end 157 | 158 | test "define abstract class" do 159 | abstract class ActiveRecord.Base do 160 | end 161 | 162 | assert_raise RuntimeError, "cannot instantiate abstract class #{ActiveRecord.Base}", fn -> 163 | ActiveRecord.Base.new 164 | end 165 | 166 | class Post < ActiveRecord.Base do 167 | var :title 168 | end 169 | 170 | assert Post.new(title: "Post 1").title == "Post 1" 171 | purge [ActiveRecord.Base, Post] 172 | end 173 | 174 | test "abstract class inheriting from abstract class" do 175 | abstract class ActiveRecord.Base do 176 | end 177 | 178 | abstract class ApplicationRecord < ActiveRecord.Base do 179 | end 180 | 181 | assert_raise RuntimeError, "cannot instantiate abstract class #{ActiveRecord.Base}", fn -> 182 | ActiveRecord.Base.new 183 | end 184 | 185 | assert_raise RuntimeError, "cannot instantiate abstract class #{ApplicationRecord}", fn -> 186 | ApplicationRecord.new 187 | end 188 | 189 | class Post < ApplicationRecord do 190 | var :title 191 | end 192 | 193 | assert Post.new(title: "Post 1").title == "Post 1" 194 | purge [ActiveRecord.Base, ApplicationRecord, Post] 195 | end 196 | 197 | test "define final class" do 198 | final class FriezaFourthForm do 199 | end 200 | 201 | assert FriezaFourthForm.new 202 | 203 | assert_raise RuntimeError, "cannot subclass final class #{FriezaFourthForm}", fn -> 204 | class FriezaFifthForm < FriezaFourthForm do 205 | end 206 | end 207 | 208 | purge [FriezaFourthForm, FriezaFifthForm] 209 | end 210 | 211 | defp purge(module) when is_atom(module) do 212 | :code.delete(module) 213 | :code.purge(module) 214 | end 215 | defp purge(modules) when is_list(modules) do 216 | Enum.each(modules, &purge/1) 217 | end 218 | end 219 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------