📜 ⬆️ ⬇️

Elixir: What does OOP look like in a functional language?

Recently, articles and discussions on the topic of parting with the PLO and the search for meaning, which Alan Kay originally put into this concept, have become more frequent.

Kay's few sayings for those who missed
I made up the term “object-oriented”, and I can tell you

OOP is a means of communication, a state of the process, an extreme late binding of all things.

I’m sorry that I’ve been coined the term “objects”. The big idea is “messaging”.

It would be much more important to develop how it should be implemented.

It was less effort than traditional early binding systems (C, C ++, Java, etc.)

I still like dynamic typing.

In connection with these discussions, it often pops up the idea that Erlang / Elixir very well meets the criteria that Kay presented to the concept of "object-oriented". But not everyone is familiar with these languages, so there is a misunderstanding of how functional languages ​​can be more object-oriented than popular C ++, Java, C #.

In this article I want to show how EO looks like on Elixir using a simple example with exercism.io .
')
Task Description
Write a small program that stores the names of the students, grouped by the number of the class in which they are studying.

In the end, you should be able to:

  • Add student name to class
  • Get a list of all students in class
  • Get a sorted list of all students in all classes. Classes should be sorted in ascending order (1, 2, 3, etc.), and students' names should be alphabetically listed.


Let's start with the tests to see what the code that calls our functions will look like. Take a look at the tests that Exercism prepared for Ruby , in which OOP has come to the point that even operators are someone's methods.

And we will write similar tests for the Elixir version of this program:
Code.load_file("school.exs")
ExUnit.start

defmodule SchoolTest do
  use ExUnit.Case, async: true
  import School, only: [add_student: 3, students_by_grade: 1, students_by_grade: 2]

  test "get students in a non existant grade" do
    school = School.new
    assert [] == school |> students_by_grade(5)
  end

  test "add student" do
    school = School.new
    school |> add_student("Aimee", 2)

    assert ["Aimee"] == school |> students_by_grade(2)
  end

  test "add students to different grades" do
    school = School.new
    school |> add_student("Aimee", 3)
    school |> add_student("Beemee", 7)

    assert ["Aimee"] == school |> students_by_grade(3)
    assert ["Beemee"] == school |> students_by_grade(7)
  end

  test "grade with multiple students" do
    school = School.new
    grade = 6
    students = ~w(Aimee Beemee Ceemee)
    students |> Enum.each(fn(student) -> school |> add_student(student, grade) end)
    
    assert students == school |> students_by_grade(grade)
  end

  test "grade with multiple students sorts correctly" do
    school = School.new
    grade = 6
    students = ~w(Beemee Aimee Ceemee)
    students |> Enum.each(fn(student) -> school |> add_student(student, grade) end)
    
    assert Enum.sort(students) == school |> students_by_grade(grade)
  end

  test "empty students by grade" do
    school = School.new
    assert [] == school |> students_by_grade
  end

  test "students_by_grade with one grade" do
    school = School.new
    grade = 6
    students = ~w(Beemee Aimee Ceemee)
    students |> Enum.each(fn(student) -> school |> add_student(student, grade) end)
    
    assert [[grade: 6, students: Enum.sort(students)]] == school |> students_by_grade
  end

  test "students_by_grade with different grades" do
    school = School.new
    everyone |> Enum.each(fn([grade: grade, students: students]) ->
      students |> Enum.each(fn(student) -> school |> add_student(student, grade) end)
    end)

    assert everyone_sorted == school |> students_by_grade
  end

  defp everyone do
    [
      [ grade: 3, students: ~w(Deemee Eeemee) ],
      [ grade: 1, students: ~w(Effmee Geemee) ],
      [ grade: 2, students: ~w(Aimee Beemee Ceemee) ]
    ]
  end

  defp everyone_sorted do
    [
      [ grade: 1, students: ~w(Effmee Geemee) ],
      [ grade: 2, students: ~w(Aimee Beemee Ceemee) ],
      [ grade: 3, students: ~w(Deemee Eeemee) ]
    ]
  end
end

, «» «» School:

    school = School.new
    school |> add_student("Aimee", 2) # => :ok
    school |> students_by_grade(2) # => ["Aimee"]
    school |> students_by_grade # => [[grade: 2, students: ["Aimee"]]]

, , , , . , . -> pipe- |>.

, , , pipe- , , . Elixir :

    school = School.new
    School.add_student(school, "Aimee", 2) # => :ok
    School.students_by_grade(school, 2) # => ["Aimee"]
    School.students_by_grade(school) # => [[grade: 2, students: ["Aimee"]]]

! «» . . , …

, , Erlang, Elixir, OTP, - , , Erlang. OTP ( ). , — GenServer. ( Erlang).

- , , . , , race condition , . — GenServer, — .

, , handle_call (c ) handle_cast ( ). , . .. API-, , .

, , (.. ).

, . , :

defmodule School do
  use GenServer

  # API

  @doc """
  Start School process.
  """
  def new do
    {:ok, pid} = GenServer.start_link(__MODULE__, %{})
    pid
  end

  @doc """
  Add a student to a particular grade in school.
  """
  def add_student(pid, name, grade) do
    GenServer.cast(pid, {:add, name, grade})
  end

  @doc """
  Return the names of the students in a particular grade.
  """
  def students_by_grade(pid, grade) do
    GenServer.call(pid, {:students_by_grade, grade})
  end

  @doc """
  Return the names of the all students separated by grade.
  """
  def students_by_grade(pid) do
    GenServer.call(pid, :all_students)
  end

  # Callbacks

  def handle_cast({:add, name, grade}, state) do
    state = Map.update(state, grade, [name], &([name|&1]))
    {:noreply, state}
  end

  def handle_call({:students_by_grade, grade}, _from, state) do
    students = Map.get(state, grade, []) |> Enum.sort
    {:reply, students, state}
  end

  def handle_call(:all_students, _from, state) do
    all_students = state
      |> Map.keys
      |> Enum.map(fn(grade) ->
        [grade: grade, students: get_students_by_grade(state, grade)]
      end)

    {:reply, all_students, state}
  end

  # Private functions

  defp get_students_by_grade(state, grade) do
    Map.get(state, grade, []) |> Enum.sort
  end
end

, , GenServer, 3 :


— pid, API-. start_link, , , ( ) new.

- system-wide , , . pid API-, .. .

Elixir , .

P.S. , Elixir , , — . « ».

Source: https://habr.com/ru/post/307720/


All Articles