- Milhouse (Renan Ranelli)
- Currently @ Podium.com
- Working with Elixir since 2015
- Organizes ElugSP
- We already have “lots” of people working with functional programming …
- … however, we still did not have enough time to consolidate “best practices” [citation needed]
- We will show one specific and not-so-trivial example and a naive non-extensible solution to it. After that, we will try to fix it
- This talk is about code. Its about stuff we do in the trenches. (and because of that, its not really “introduction” material)
- The Why
- Names & Verbs
- Protocols
- Data abstraction
- SOLID
- Conclusion
- We want our software/system to have certain properties in order to make our lives as developers less miserable
- We want:
- Comprehensibility (legibility++)
- Flexibility
- ((((*((Extensibility))))*))
- Production Stability
- Google-ability
- Scalability
- **-ity
- And of course, you can’t have it all. You gotta choose your trade-off.
> Extensible, or modular code, is code that can be modified, > interacted with, added to, or manipulated … all > without ever modifying the core code base.
- A good trick is to think about code that is easy to delete instead of code that is easy to write
- That is: code that hurts you less if it is wrong. (and surprise, the code will eventually be wrong)
- OO e FP are not that different.
- The main difference resides in how you organize programs
- In OO’s case, we do it around names
- In FP’s case, we do it around verbs
- Small objects gravitate towards closures; Rich closures gravitate towards objects;
- (Steve Yegge has an awesome & classic text on this. Google up: “execution in the kingdom of nouns”)
- Well take an example in simple “geometry” to base our discussion.
- Lets take a very simple plane geometry system:
- Three entities: Lines (infinite), Points and “Nothing”.
- One operation : Intersection
defmodule Geometry do
defmodule Point, do: (defstruct [:x, :y])
defmodule Line, do: (defstruct [:a, :b]) # y = ax + b
defmodule Nothing, do: (defstruct [])
# ordering *is* important
def intersection(thing, ^thing),
do: thing
def intersection(p = %Point{}, _oto_p = %Point{}),
do: %Nothing{}
def intersection(p = %Point{}, l = %Line{}) do
if (p.y == (l.a * p.x) + l.b) do
p
else
%Nothing{}
end
end
def intersection(%Line{a: a}, %Line{a: a}),
do: %Nothing{}
def intersection(l = %Line{}, l2 = %Line{}) do
# You can ignore the next two lines of math
x_intersection = (l.b - l2.b) / (l.a - l2.a)
y_intersection = (l2.a * x_intersection) + l2.b
%Point{x: x_intersection, y: y_intersection}
end
def intersection(%Nothing{}, _other),
do: %Nothing{}
# trick: intersection is commutative! (a <> b = b <> a)
def intersection(right, left),
do: intersection(left, right)
end
class Nothing
def intersection(_other_shape)
Nothing.new
end
end
class Line
# y=ax+b
def initialize(a, b)
@a, @b = a, b
end
attr_reader :a, :b
def intersection(other_shape)
# This technique is called *double dispatch*
other_shape.intersect_with_line(self)
end
def intersect_with_line(other_line)
if other_line.a == self.a && other_line.b == self.b then
self
else
Nothing.new
end
end
def intersection_with_point(other_point)
if other_point.y == self.a * other_point.x + self.b then
other_point
else
Nothing.new
end
end
end
class Point
def initialize(x, y)
@x, @y = x, y
end
attr_reader :x, :y
def intersection(other_shape)
other_shape.intersection_with_point(self)
end
def intersection_with_point(other_point)
if other_point.x == self.x && other_point.y == self.y then
self
else
Nothing.new
end
end
def intersect_with_line(other_line)
other_line.intersection_with_point(self)
end
end
- Now we need line segments.
defmodule Geometry do
# ...
defmodule Segment, do: (defstruct [:a, :b, :xmin, :xmax])
# ...
def intersection(p = %Point{}, s = %Segment{}) do
in_x? = (p.x >= s.xmin && p.x <= s.xmax)
in_line? = (p.y == (s.a * p.x) + s.b)
if in_x? && in_line? do
p
else
%Nothing{}
end
end
def intersection(l = %Line{a: a, b: b}, s = %Segment{a: a, b: b}),
do: s
def intersection(l = %Line{a: a, b: b}, s = %Segment{a: a, b: _b}),
do: %Nothing{}
def intersection(l = %Line{}, s = %Segment{}) do
# ra*x + rb = sa*x + sb => x = (sb - rb) / (ra-sa)
x_intersection = (s.b - l.b) / (l.a - s.a)
if x_intersection >= s.xmin && x_intersection <= s.xmax do
y_intersection = (s.a * x_intersection) + s.b
%Point{x: x_intersection, y: y_intersection}
else
%Nothing{}
end
end
# ...
end
class Nothing
end
class Line
def intersection_with_segment(segment)
# ...
end
end
class Point
def intersection_with_segment(segment)
# ...
end
end
class Segment
def intersection(other_shape)
other_shape.intersection_with_segment(self)
end
def intersection_with_point(other_point)
# ...
end
def intersect_with_line(other_line)
# ...
end
end
- Now we need to compute the “length” of the geometric shape
defmodule Geometry do
# ...
def length(%Point{}), do: 0
def length(%Nothing{}), do: 0
def length(%Line{}), do: 1_000_000
def length(s = %Segment{}) do
adjacent_catet =
(s.xmax - s.xmin)
opposed_catet =
s.a * (s.xmax - s.xmin)
hypotenuse =
:math.sqrt(:math.pow(adjacent_catet, 2) +
:math.pow(opposed_catet, 2))
hypotenuse
end
# ...
end
class Nothing
def length
0
end
end
class Line
def length
1_000_000 # because 1 million is big enough
end
end
class Point
def length
0
end
end
class Segment
def length
# ...
end
end
- When adding a new operation (that is, a verb)
- FP approach needs to add a single function
- OO approach needs to change all objects
- When adding a new entity (that is, a name)
- FP approach needs to change all functions
- OO approach needs to add a single object
- If you have control over both data & behaviour, than this is not gonna hurt you that much (yet)
- Now, consider that this code is to be part of a library
- The FP version is not extensible. The OO version is.
- Is a solution to the problem we described.
- It is a way to extend the behaviour of a function without touching its definition, based on the type of its first argument
- Its basically “dynamic dispatch one level deep”
# "escopo/biblioteca/application/outro time cuida"
defmodule X, do: (defstruct [:x])
defmodule Y, do: (defstruct [y: 1])
defprotocol StarsEncoder do
def encode(data)
end
# ...
defimpl StarsEncoder, for: Z do
def encode(x), do: " * "
end
defimpl StarsEncoder, for: X do
def encode(x), do: " * "
end
defimpl StarsEncoder, for: Y do
def encode(y) do
stars = Range.new(0, y.y) |> Enum.map(fn _ -> "*" end) |> Enum.join
" #{stars} "
end
end
# biblitoeca
defprotocol Measurable do
def length(forma)
end
# these can be in completely different files
defimpl Measurable, for: Line do
def length(_line_), do: 1_000_000
end
defimpl Measurable, for: Point do
def length(_point), do: 0
end
defimpl Measurable, for: Nothing do
def length(_nothing), do: 0
end
defimpl Measurable, for: Segment do
def length(segment) do
adjacent_catet = (s.xmax - s.xmin)
opposed_catet = s.a * (s.xmax - s.xmin)
hypotenuse =
:math.sqrt(:math.pow(adjacent_catet, 2) + :math.pow(opposed_catet, 2))
hypotenuse
end
end
defprotocol Intersectable do
def intersection(forma, other_shape)
end
defimpl Intersectable, for: Nothing do
def intersection(%Nothing{}, _other), do: %Nothing{}
end
defimpl Intersectable, for: Point do
def intersection(p, p), do: p
def intersection(p, _oto), do: %Nothing{}
# cachorragem
def intersection(left, right),
do: Intersectable.intersection(right, left)
end
defimpl Intersectable, for: Line do
def intersection(l, l), do: l
def intersection(%Line{a: a}, %Line{a: a}), do: %Nothing{}
def intersection(l, l2 = %Line{}) do
x_intersection = (l.b - l2.b) / (l.a - l2.a)
y_intersection = (l2.a * x_intersection) + l2.b
%Point{x: x_intersection, y: y_intersection}
end
def intersection(l, p = %Point{}) do
if (p.y == (l.a * p.x) + l.b) do
p
else
%Nothing{}
end
end
# trick
def intersection(left, right),
do: Intersectable.intersection(right, left)
end
defimpl Intersectable, for: Segment do
def intersection(s = %Segment{a: a, b: b}, l = %Line{a: a, b: b}), do: s
def intersection(s = %Segment{a: a}, l = %Line{a: a}), do: %Nothing{}
def intersection(s = %Segment{}, l = %Line{}) do
# ra*x + rb = sa*x + sb => x = (sb - rb) / (ra-sa)
x_intersection = (s.b - l.b) / (l.a - s.a)
if x_intersection >= s.xmin && x_intersection <= s.xmax do
y_intersection = (s.a * x_intersection) + s.b
%Point{x: x_intersection, y: y_intersection}
else
%Nothing{}
end
end
def intersection(s = %Segment{}, p = %Point{}) do
in_x? = (p.x >= s.xmin && p.x <= s.xmax)
in_line? = (p.y == (s.a * p.x) + s.b)
if in_x? && in_line? do
p
else
%Nothing{}
end
end
end
defmodule Drawing do
defstruct [:pieces] # what are pieces? Doesn't matter
# The only thing we care is that they are *measurable*
def perimeter(drawing) do
drawing.pieces |> Enum.map(&Measurable.length/1) |> Enum.sum
end
end
defmodule Poligon do
defstruct [:segments]
def new(segments) do
assert Enum.all?(segments, &match(%Segment{}, &1))
%Poligon{segments: segments}
end
def perimeter(poligon) do
%Drawing{pieces: poligon.segments} |> Drawing.perimeter
end
end
- As you can see, our new solution is naturally prepared to deal with new data types it doesn’t know. (e.g. the commutative trick)
- It is possible to extend existing behaviour with new data.
And it is trivial to create new data types that are
extensible with new behaviours. Its a win-win situation
(Ruby solves this with “open classes & duck typing”. A much less elegant solution)
- Our software is now more abstract, and relies less on concrete implementations. (Program towards interfaces)
- We just recovered one of the main benefits of OO back into
our FP code.
**IT IS IMPORTANT TO UNDERSTAND WHY THINGS ARE THE WAY THEY ARE**
> wat. isn’t SOLID an OO thing?
- Nah.
- [S]ingle responsibility principle (Basic higiene)
- [O]pen closed principle (Protocols)
- [L]iskov substitution principle (…)
- [I]nterface segregation principle (Data abstraction)
- [D]ependency injection principle (Composition)
- Throwing in a new paradigm into your programming practice will not magically make you write good code.
- Your knowledge in X can be useful in Y. First principles first.
- Parsimony. If you use protocols for absolutely everything, your program will end up as a bad replica of an OO solution.
- Indirection, encapsulation, abstraction, extensibility … its all trade-offs; (Like everything else in engineering.)
Twitter: @renanranelli Github: /rranelli Blog: milhouse.dev