├── .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 |
2 |
3 | # OOP
4 |
5 | [](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 | [](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 |
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 |
--------------------------------------------------------------------------------