Repository: wojtekmach/oop Branch: master Commit: b1ce8bf1dff7 Files: 7 Total size: 14.0 KB Directory structure: gitextract_t24ek6r3/ ├── .gitignore ├── .travis.yml ├── README.md ├── lib/ │ └── oop.ex ├── mix.exs └── test/ ├── oop_test.exs └── test_helper.exs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /_build /cover /deps erl_crash.dump *.ez ================================================ FILE: .travis.yml ================================================ language: elixir sudo: false elixir: - 1.3.4 otp_release: - 18.2 ================================================ FILE: README.md ================================================


oop
# OOP [![Build Status](https://travis-ci.org/wojtekmach/oop.svg?branch=master)](https://travis-ci.org/wojtekmach/oop) 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]! ## Demo [![Lightning Talks - Wojtek Mach (ElixirConfEU 2016)](https://img.youtube.com/vi/5EtV2JUU0Z4/0.jpg)](https://www.youtube.com/watch?v=5EtV2JUU0Z4) ## Example ```elixir import OOP class Person do var :name def say_hello_to(who) do what = "Hello #{who.name}" IO.puts("#{this.name}: #{what}") end end joe = Person.new(name: "Joe") mike = Person.new(name: "Mike") robert = Person.new(name: "Robert") joe.say_hello_to(mike) # Joe: Hello Mike mike.say_hello_to(joe) # Mike: Hello Joe mike.say_hello_to(robert) # Mike: Hello Robert robert.say_hello_to(mike) # Robert: Hello Mike joe.set_name("Hipster Joe") joe.name # => Hipster Joe ``` An OOP library wouldn't be complete without inheritance: ```elixir class Animal do var :name end class Dog < Animal do var :breed end snuffles = Dog.new(name: "Snuffles", breed: "Shih Tzu") snuffles.name # => "Snuffles" snuffles.breed # => "Shih Tzu" ``` ... or multiple inheritance: ```elixir class Human do var :name end class Horse do var :horseshoes_on? end class Centaur < [Human, Horse] do end john = Centaur.new(name: "John", horseshoes_on?: true) john.name # => "John" john.horseshoes_on? # => true ``` See more usage in the [test suite](test/oop_test.exs). ## Installation Add `oop` to your list of dependencies in `mix.exs`: ```elixir def deps do [{:oop, "~> 0.1.0"}] end ``` [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". [2] Please don't. You've been warned. ## License The MIT License (MIT) Copyright (c) 2015 Wojciech Mach 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. ================================================ FILE: lib/oop.ex ================================================ defmodule OOP.Registry do def start_link do Agent.start_link(fn -> %{} end, name: __MODULE__) end def register(pid, class) do Agent.update(__MODULE__, &Map.put(&1, pid, class)) end def get(pid) do Agent.get(__MODULE__, &Map.get(&1, pid, nil)) end end defmodule OOP.Application do use Application def start(_type, _args) do OOP.Registry.start_link() end end defmodule OOP.Builder do def create_class(class, superclasses, block, opts) do quote do defmodule unquote(class) do OOP.Builder.ensure_can_be_subclassed(unquote(superclasses)) @final Keyword.get(unquote(opts), :final, false) def __final__? do @final end def new(data \\ [], descendant? \\ false) do OOP.Builder.ensure_can_be_instantiated(unquote(class), descendant?, unquote(opts)) object = :"#{unquote(class)}#{:erlang.unique_integer()}" defmodule object do use GenServer def start_link(data) do GenServer.start_link(__MODULE__, data, name: __MODULE__) end def class do unquote(class) end def methods do built_ins = [ code_change: 3, handle_call: 3, handle_cast: 2, handle_info: 2, init: 1, start_link: 1, terminate: 2, class: 0, methods: 0, ] __MODULE__.__info__(:functions) -- built_ins end import Kernel, except: [def: 2] Module.register_attribute(__MODULE__, :friends, accumulate: true) unquote(block) Enum.each(unquote(superclasses), fn superclass -> parent = superclass.new(data, true) for {method, arity} <- parent.methods do Code.eval_quoted(OOP.Builder.inherit_method(method, arity, parent), [], __ENV__) end end) end {:ok, pid} = object.start_link(Enum.into(data, %{})) OOP.Registry.register(pid, unquote(class)) object end end end end def ensure_can_be_subclassed(superclasses) do Enum.each(superclasses, fn s -> if s.__final__?, do: raise "cannot subclass final class #{s}" end) end def ensure_can_be_instantiated(class, descendant?, opts) do abstract? = Keyword.get(opts, :abstract, false) if !descendant? and abstract? do raise "cannot instantiate abstract class #{class}" end end def create_method(call, expr) do # HACK: this is a really gross way of checking if the function is using `this`. # if so, we let it leak: `var!(this) = data`. # We do this so that we don't get the "unused variable this" warning when # we don't use `this`. using_this? = String.match?(Macro.to_string(expr), ~r"\bthis\.") {method, args} = Macro.decompose_call(call) handle_call_quoted = quote do try do [do: value] = unquote(expr) {:reply, {:ok, value}, data} rescue e in [RuntimeError] -> {:reply, {:error, e}, data} end end quote do def unquote(call) do case GenServer.call(__MODULE__, {:call, unquote(method), unquote(args)}) do {:ok, value} -> value {:error, e} -> raise e end end if unquote(using_this?) do def handle_call({:call, unquote(method), unquote(args)}, _from, data) do var!(this) = data unquote(handle_call_quoted) end else def handle_call({:call, unquote(method), unquote(args)}, _from, data) do unquote(handle_call_quoted) end end end end def inherit_method(method, arity, parent) do args = (0..arity) |> Enum.drop(1) |> Enum.map(fn i -> {:"arg#{i}", [], OOP} end) {:defdelegate, [context: OOP, import: Kernel], [{method, [], args}, [to: parent]]} end def create_var(field, opts) do private? = Keyword.get(opts, :private, false) quote do def unquote(field)() do case GenServer.call(__MODULE__, {:get, unquote(field)}) do {:ok, value} -> value {:error, :private} -> raise "Cannot access private var #{unquote(field)}" end end def unquote(:"set_#{field}")(value) do GenServer.call(__MODULE__, {:set, unquote(field), value}) end def handle_call({:get, unquote(field)}, {pid, _ref}, data) do classes = [class() | @friends] if unquote(private?) and ! OOP.Registry.get(pid) in classes do {:reply, {:error, :private}, data} else {:reply, {:ok, Map.get(data, unquote(field))}, data} end end def handle_call({:set, unquote(field), value}, _from, data) do {:reply, value, Map.put(data, unquote(field), value)} end end end end defmodule OOP do defmacro class(class_expr, block, opts \\ []) do {class, superclasses} = case class_expr do {:<, _, [class, superclasses]} when is_list(superclasses) -> {class, superclasses} {:<, _, [class, superclass]} -> {class, [superclass]} class -> {class, []} end OOP.Builder.create_class(class, superclasses, block, opts) end defmacro abstract(class_expr, block) do {:class, _, [class]} = class_expr quote do OOP.class(unquote(class), unquote(block), abstract: true) end end defmacro final(class_expr, block) do {:class, _, [class]} = class_expr quote do OOP.class(unquote(class), unquote(block), final: true) end end defmacro def(call, expr \\ nil) do OOP.Builder.create_method(call, expr) end defmacro var(field, opts \\ []) do OOP.Builder.create_var(field, opts) end defmacro private_var(field) do quote do var(unquote(field), private: true) end end defmacro friend(class) do quote do @friends unquote(class) end end end ================================================ FILE: mix.exs ================================================ defmodule OOP.Mixfile do use Mix.Project def project do [app: :oop, version: "0.1.1", description: "OOP in Elixir!", package: package(), elixir: "~> 1.0", build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, deps: deps()] end def application do [mod: {OOP.Application, []}] end defp deps do [] end defp package do [ maintainers: ["Wojtek Mach"], licenses: ["MIT"], links: %{"GitHub" => "https://github.com/wojtekmach/oop"}, ] end end ================================================ FILE: test/oop_test.exs ================================================ defmodule OOPTest do use ExUnit.Case import OOP test "define empty class" do c = class Person do end assert c purge Person end test "instantiate empty object" do class Person do end alice = Person.new assert alice.class == Person purge Person end test "define methods on objects" do class Person do def zero do 0 end def sum1(a) do a end def sum2(a, b) do a + b end end alice = Person.new assert alice.zero() == 0 assert alice.sum1(1) == 1 assert alice.sum2(1, 2) == 3 purge Person end test "define fields" do class Person do var :name def title(prefix) do "#{prefix} #{this.name}" end end alice = Person.new assert alice.name == nil bob = Person.new(name: "Bob") assert bob.name == "Bob" bob.set_name("Hipster Bob") assert bob.name == "Hipster Bob" assert bob.title("Mr.") == "Mr. Hipster Bob" assert alice.name == nil purge Person end test "define private fields" do class AppleInc do private_var :registered_devices def registered_devices_count do length(this.registered_devices) end end apple = AppleInc.new(registered_devices: ["Alice's iPhone", "Bob's iPhone"]) assert_raise RuntimeError, "Cannot access private var registered_devices", fn -> apple.registered_devices end assert apple.registered_devices_count == 2 purge AppleInc end test "define friend class" do class NSA do def get_data(company) do company.registered_devices end end class Thief do def get_data(company) do company.registered_devices end end class AppleInc do friend NSA private_var :registered_devices end apple = AppleInc.new(registered_devices: ["Alice's iPhone", "Bob's iPhone"]) thief = Thief.new nsa = NSA.new assert_raise RuntimeError, "Cannot access private var registered_devices", fn -> thief.get_data(apple) end assert nsa.get_data(apple) == ["Alice's iPhone", "Bob's iPhone"] purge [AppleInc, Thief, NSA] end test "inheritance" do class Animal do var :name def title(prefix) do "#{prefix} #{this.name}" end end class Dog < Animal do var :breed end snuffles = Dog.new(name: "Snuffles", breed: "Shih Tzu") assert snuffles.name == "Snuffles" assert snuffles.breed == "Shih Tzu" assert snuffles.title("Mr.") == "Mr. Snuffles" purge [Animal, Dog] end test "multiple inheritance" do class Human do var :name end class Horse do var :horseshoes_on? end class Centaur < [Human, Horse] do end john = Centaur.new(name: "John", horseshoes_on?: true) assert john.name == "John" assert john.horseshoes_on? == true purge [Human, Horse, Centaur] end test "define abstract class" do abstract class ActiveRecord.Base do end assert_raise RuntimeError, "cannot instantiate abstract class #{ActiveRecord.Base}", fn -> ActiveRecord.Base.new end class Post < ActiveRecord.Base do var :title end assert Post.new(title: "Post 1").title == "Post 1" purge [ActiveRecord.Base, Post] end test "abstract class inheriting from abstract class" do abstract class ActiveRecord.Base do end abstract class ApplicationRecord < ActiveRecord.Base do end assert_raise RuntimeError, "cannot instantiate abstract class #{ActiveRecord.Base}", fn -> ActiveRecord.Base.new end assert_raise RuntimeError, "cannot instantiate abstract class #{ApplicationRecord}", fn -> ApplicationRecord.new end class Post < ApplicationRecord do var :title end assert Post.new(title: "Post 1").title == "Post 1" purge [ActiveRecord.Base, ApplicationRecord, Post] end test "define final class" do final class FriezaFourthForm do end assert FriezaFourthForm.new assert_raise RuntimeError, "cannot subclass final class #{FriezaFourthForm}", fn -> class FriezaFifthForm < FriezaFourthForm do end end purge [FriezaFourthForm, FriezaFifthForm] end defp purge(module) when is_atom(module) do :code.delete(module) :code.purge(module) end defp purge(modules) when is_list(modules) do Enum.each(modules, &purge/1) end end ================================================ FILE: test/test_helper.exs ================================================ ExUnit.start()