Day 4 of the Elm Advent Calendar.
The third day was @ arowM's Handling special clicks with SPA routing.
I feel like I'm writing this kind of article endlessly, but this time I'd like to learn functional programming by touching an object-oriented language, but I'm starting to learn functional programming for those who think it's heavy. , This article is for those who do not know how to write. Since an object-oriented language is too broad, I took a Java program as a sample and wrote it with an awareness of how to express it in the pure functional language Elm.
Class
In functional languages, programming is done by combining functions and data structures. In Java, programming is done by defining classes. If you look only at this explanation, what is the difference between functional programming and C language? I'm wondering if object-oriented and functional languages are completely different languages, but that's not the case. First of all, let's prepare a simple class. A `` `Personclass that overloads fields and some methods defined in * Getter *, * Setter * and the
Object``` class. Note that the "always" method returns a value. Isn't * Setter * impossible? You might think that it is designed as a ** immutable object ** by returning a new object with a new value set.
// new Person("Takeshi", 15)
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public Person setName(String name) {
return new Person(name, age);
}
public int getAge() {
return age;
}
public Person setAge(int age) {
return new Person(name, age);
}
public String introduce() {
String hi = "Hi! ";
String n = "My name is " + name + "! ";
String a = "My age is " + age + ".";
return hi + n + a;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
if (age != person.age) return false;
return name != null ? name.equals(person.name) : person.name == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
@Override
public String toString() {
return "Person \"" + name + "\" " + age;
}
}
Now let's reproduce the `Person``` class in Elm.
type Person = Person Name Age
`The definition of the data itself is completed in just one line. This is called * union type *, and define the field definition and constructor, and define
toString```, ```equals```, ``
hashCode at once on the language (Elm) side. I will. `` `Person" Takeshi "15
You can create an instance (although not) by writing it like this. `` `Nameand
Ageare just type aliases of
Stringand
Int```, but just adding alias makes the definition very easy to see. (I want it when I do DDD in Java too).
module Person exposing (Person(..), getName, setName, getAge, setAge)
type alias Name =
String
type alias Age =
Int
{-| Person "Takeshi" 15
(Person "Takeshi" 15 |> toString) == "Person "Takeshi" 15"
-}
type Person
= Person Name Age
In Elm, a method is represented by a function. What is a methodA function that also takes arguments and the object itself as argumentsCan be considered to be. The order of the arguments does not change, butArgument 1->Argument 2->Argument 3... ->object->Return type
Elm recommends defining in this order. The secret is the pipe operator(|>)It is in. The pipe operator can be used to flow function arguments from the left side to the right side.x |> f == f x
Pipes make it easier to write code like method chains.
-- setName : Name -> Person -> Person
setName "John" (Person "Takeshi" 15 ) == Person "John" 15
-- new Person("Takeshi", 15).setName("John")You can read it in a flowing manner!
(Person "Takeshi" 15 |> setName "John") == Person "John" 15
Let's take a look at the `Person``` type methods (functions). Types written in * union type * can be decomposed field by field using a syntax called pattern matching. Unused fields can be ignored with
_```. The ``
let in``` syntax defines a local variable in the let clause and writes an expression that returns the final return value in in.
{-| (Person "Takeshi" 15 |> getName) == "Takeshi"
-}
getName : Person -> Name
getName (Person name _) =
name
{-| (Person "Takeshi" 15 |> setName "John") == Person "John" 15
-}
setName : Name -> Person -> Person
setName name (Person _ age) =
Person name age
{-| (Person "Takeshi" 15 |> getAge) == 15
-}
getAge : Person -> Age
getAge (Person _ age) =
age
{-| (Person "Takeshi" 15 |> setAge 20) == Person "John" 20
-}
setAge : Age -> Person -> Person
setAge age (Person name _) =
Person name age
{-| (Person "Takeshi" 15 |> introduce) == "Hi! My name is Takeshi! My age is 15."
-}
introduce : Person -> String
introduce (Person name age) =
let
hi =
"Hi! "
n =
"My name is " ++ name ++ "! "
a =
"My age is " ++ toString age ++ "."
in
hi ++ n ++ a
The syntax may be a little annoying, but is that? What you do in OOP and FP is the same, right? Do you feel like saying that? This is the basic form of thinking. Also, for data structures like Person, * Getter *, * Setter * can be written more simply by using the predefined record syntax. The writing method is similar to Json, and it can be accessed in the form of `` `.field```, so it is more like an object-oriented writing method. Pattern matching syntax for records is also provided.
type alias Person = { name : Name, age : Age }
--Record generation
takeshi : Person
takeshi = { name = "Takeshi", age = "15" }
-- Getter
> takeshi.name
"Takeshi" : String
-- Setter
> { takeshi | age = 20 }
{ name = "Takeshi", age = 20 } : { name : String, age : number }
introduce : Person -> String
introduce { name, age } =
let
hi =
"Hi! "
n =
"My name is " ++ name ++ "! "
a =
"My age is " ++ toString age ++ "."
in
hi ++ n ++ a
Composition
After defining the class, let's reproduce the composition. Here is the sample code in Java. It's heavy when I try to write it properly (I don't use a library such as Lombok so that everyone can understand it).
package composition;
// new Foo(5, new Bar(6, 7));
public class Foo {
private final int x;
private final Bar bar;
public Foo(int x, Bar bar) {
this.x = x;
this.bar = bar;
}
public int getX() {
return x;
}
public Foo setX(int x) {
return new Foo(x, bar);
}
public int getY() {
return bar.getY();
}
public Foo setY(int y) {
return new Foo(x, bar.setY(y));
}
public int getZ() {
return bar.getZ();
}
public Foo setZ(int z) {
return new Foo(x, bar.setZ(z));
}
public Bar getBar() {
return bar;
}
public int calc() {
return x + bar.getY() + bar.getZ();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Foo foo = (Foo) o;
if (x != foo.x) return false;
return bar != null ? bar.equals(foo.bar) : foo.bar == null;
}
@Override
public int hashCode() {
int result = x;
result = 31 * result + (bar != null ? bar.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Foo " + x + " (" + bar + ')';
}
}
package composition;
public class Bar {
private final int y;
private final int z;
public Bar(int y, int z) {
this.y = y;
this.z = z;
}
public int getY() {
return y;
}
public Bar setY(int y) {
return new Bar(y, z);
}
public int getZ() {
return z;
}
public Bar setZ(int z) {
return new Bar(y, z);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Bar bar = (Bar) o;
if (y != bar.y) return false;
return z == bar.z;
}
@Override
public int hashCode() {
int result = y;
result = 31 * result + z;
return result;
}
@Override
public String toString() {
return "Bar " + y + ' ' + z;
}
}
It is basically the same as what was explained in Class. Just bring the Bar
type to the field. Pattern matching can decompose the structure without any problem even if * union type * is nested.
module Foo exposing (..)
import Bar exposing (..)
{-| Foo 5 (Bar 6 7)
-}
type Foo
= Foo Int Bar
getX : Foo -> Int
getX (Foo x _) =
x
setX : Foo -> Int -> Foo
setX (Foo _ bar) x =
Foo x bar
getY : Foo -> Int
getY (Foo _ (Bar y _)) =
y
setY : Foo -> Int -> Foo
setY (Foo x (Bar _ z)) y =
Foo x (Bar y z)
getZ : Foo -> Int
getZ (Foo _ (Bar _ z)) =
z
setZ : Foo -> Int -> Foo
setZ (Foo x (Bar y _)) z =
Foo x (Bar y z)
{-| (Foo 5 (Bar 6 7) |> calc) == 18
-}
calc : Foo -> Int
calc (Foo x (Bar y z)) =
x + y + z
The structure can be handled transparently by using pattern matching, and the `` `Bar``` type on the side to be composed does not have any function. Of course, if the structure and usage are complicated, it is better to share the responsibility of the function for each module as in the Java example. It is also possible to code with encapsulation in mind. You can nest records as described in Class above (although updating is a bit more complicated). The freedom of writing around here is exactly the same as Java (I was surprised when I found out that I could do it with the same feeling).
module Bar exposing (..)
type Bar
= Bar Int Int
Polymophism
I think that you can understand the encapsulation and the use of modules through how to make Elm-style classes and Composition. Of course, I don't think I'm satisfied with this alone. Let's reproduce Polymorphism using the interface with Elm. Let's use the Tree and Visitor patterns, which make simple calculations, as sample code. To simplify the example, the formula only supports numbers and addition.
package polymorphism;
public class NodeTest {
public static void main(String[] args) {
Node tree = new AddNode(
new AddNode(
new NumNode(2),
new NumNode(3)),
new NumNode(4)
);
// ((2 + 3) + 4) = 9
System.out.println(tree.accept(new Calculator()));
}
}
package polymorphism;
public interface Node {
int accept(Visitor visitor);
}
package polymorphism;
public class NumNode implements Node {
private final int value;
public NumNode(int value) {
this.value = value;
}
public int getValue() {
return value;
}
@Override
public int accept(Visitor visitor) {
return visitor.visit(this);
}
}
package polymorphism;
public class AddNode implements Node {
private final Node left;
private final Node right;
public Node getLeft() {
return left;
}
public Node getRight() {
return right;
}
public AddNode(Node left, Node right) {
this.left = left;
this.right = right;
}
@Override
public int accept(Visitor visitor) {
return visitor.visit(this);
}
}
package polymorphism;
public interface Visitor {
int visit(NumNode node);
int visit(AddNode node);
}
package polymorphism;
public class Calculator implements Visitor {
@Override
public int visit(NumNode node) {
return node.getValue();
}
@Override
public int visit(AddNode node) {
int left = node.getLeft().accept(this);
int right = node.getRight().accept(this);
return left + right;
}
}
Elm is really good at code that makes full use of polymorphism such as the Visitor pattern. It is a * union type * that I have been using until now, but it shows its true character by using the direct sum of types. `type Node = Num Int | Add Node Node ``` The
Node``` type consists of two types, ``` Num``` and ```Add```, and further
ʻAddhas a recursive structure with two of its own
Nodetypes. The form itself is the same as the Java
Node type, but it is used differently. Let's take a look at the `` `calc
function. You can see the new syntax `case node of`
. This is a type of pattern matching that is written when used as an expression rather than an argument. It is used to classify the direct sum type of * union type *. From a different point of view, you can see that it implements an interface with `calc``` methods on
Num``` and ```Add``` types. Also, in the case of ```Add``` type, you can see that ``
calc``` is recursively called to find the values on the right and left sides. The tree can be intuitively represented by * union type *.
module Node exposing (..)
import Html exposing (..)
type Node
= Num Int
| Add Node Node
calc : Node -> Int
calc node =
case node of
Num n ->
n
Add l r ->
let
lValue =
calc l
rValue =
calc r
in
lValue + rValue
main : Html msg
main =
let
tree =
Add
(Add
(Num 2)
(Num 3)
)
(Num 4)
in
text <| toString <| calc tree
It may take some getting used to, but basically you can easily reproduce what you were doing object-oriented with a function that uses * union type * + pattern matching. There are many cases where you can write very concisely, so let's master it.
Collection
Let's compare Collection using jshell and Java9. The execution results are also shown, so please feel the difference.
List
List.java
jshell> IntStream.rangeClosed(1, 10).mapToObj(n -> n * 2).collect(Collectors.toList())
$1 ==> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
jshell> IntStream.rangeClosed(1, 10).filter(n -> n % 2 == 0).boxed().collect(Collectors.toList())
$2 ==> [2, 4, 6, 8, 10]
jshell> List<Integer> list = IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList())
$3 ==> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
jshell> Collections.reverse($3)
jshell> $3
$3 ==> [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
jshell> IntStream.rangeClosed(1, 10).limit(5).boxed().collect(Collectors.toList())
$4 ==> [1, 2, 3, 4, 5]
jshell> IntStream.rangeClosed(1, 10).skip(5).boxed().collect(Collectors.toList()
$5 ==> [6, 7, 8, 9, 10]
jshell> Stream.generate(() -> "a").limit(10).collect(Collectors.toList())
$6 ==> [a, a, a, a, a, a, a, a, a, a]
Stream.concat(Stream.concat(Stream.of(1, 2), Stream.of(3)), Stream.of(4, 5)).collect(Collectors.toList())
$7 ==> [1, 2, 3, 4, 5]
jshell> IntStream.rangeClosed(1, 5).flatMap(i -> IntStream.of(0, i)).boxed().skip(1).collect(Collectors.toList())
$8 ==> [1, 0, 2, 0, 3, 0, 4, 0, 5]
jshell> IntStream.rangeClosed(1, 10).boxed().collect(Collectors.partitioningBy(n -> n % 2 == 0))
$9 ==> {false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8, 10]}
jshell> String[] alphas = {"a", "b", "c", "d", "e"}
alphas ==> String[5] { "a", "b", "c", "d", "e" }
jshell> IntStream.range(0, 5).boxed().collect(Collectors.toMap(i -> i, i -> alphas[i])).entrySet()
$10 ==> [0=a, 1=b, 2=c, 3=d, 4=e]
List.elm
> List.range 1 10 |> List.map (\n -> n * 2)
[2,4,6,8,10,12,14,16,18,20] : List Int
> List.range 1 10 |> List.filter (\n -> n % 2 == 0)
[2,4,6,8,10] : List Int
> List.range 1 10 |> List.reverse
[10,9,8,7,6,5,4,3,2,1] : List Int
> List.range 1 10 |> List.take 5
[1,2,3,4,5] : List Int
> List.range 1 10 |> List.drop 5
[6,7,8,9,10] : List Int
> List.repeat 10 "a"
["a","a","a","a","a","a","a","a","a","a"] : List String
> List.concat [[1,2],[3],[4,5]]
[1,2,3,4,5] : List number
> List.range 1 5 |> List.intersperse 0
[1,0,2,0,3,0,4,0,5] : List Int
> List.range 1 10 |> List.partition (\n -> n % 2 == 0)
([2,4,6,8,10],[1,3,5,7,9]) : ( List Int, List Int )
> List.indexedMap (,) ["a","b","c","d","e"]
[(0,"a"),(1,"b"),(2,"c"),(3,"d"),(4,"e")] : List ( Int, String )
Map
Map.java
jshell> Map.of("a", 1, "b", 2, "c", 3)
$1 ==> {b=2, c=3, a=1}
jshell> Map.of("a", 1, "b", 2, "c", 3).keySet()
$2 ==> [c, b, a]
jshell> Map.of("a", 1, "b", 2, "c", 3).values()
$3 ==> [3, 2, 1]
jshell> Map.of("a", 1, "b", 2, "c", 3).containsKey("a")
$4 ==> true
jshell> Map.of("a", 1, "b", 2, "c", 3).get("a")
$5 ==> 1
jshell> Map.of("a", 1, "b", 2, "c", 3).get("d")
$6 ==> null
jshell> Map.of("a", 1, "b", 2, "c", 3).getOrDefault("d", -1)
$7 ==> -1
jshell> HashMap<String, Integer> hash = new HashMap<>(Map.of("a", 1, "b", 2, "c", 3)))
hash ==> {a=1, b=2, c=3}
jshell> hash.put("d", 4)
$8 ==> null
jshell> hash.get("d")
$9 ==> 4
jshell> HashMap<String, Integer> hash = new HashMap<>(Map.of("a", 1, "b", 2, "c", 3))
hash ==> {a=1, b=2, c=3}
jshell> hash.replace("a", hash.get("a") + 10)
$10 ==> 1
jshell> hash
hash ==> {a=11, b=2, c=3}
jshell> HashMap<String, Integer> hash = new HashMap<>(Map.of("a", 1, "b", 2, "c", 3))
hash ==> {a=1, b=2, c=3}
jshell> hash.remove("a")
$35 ==> 1
jshell> hash
hash ==> {b=2, c=3}
//From this point onward, it's too slippery and I give up
Map.elm
> Dict.fromList [("a",1),("b",2),("c",3)]
Dict.fromList [("a",1),("b",2),("c",3)] : Dict.Dict String number
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.keys
["a","b","c"] : List String
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.values
[1,2,3] : List number
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.member "a"
True : Bool
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.get "a"
Just 1 : Maybe.Maybe number
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.get "d"
Nothing : Maybe.Maybe number
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.get "d" |> Maybe.withDefault -1
-1 : number
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.insert "d" 4 |> Dict.get "d"
Just 4 : Maybe.Maybe number
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.update "a" (\m -> Maybe.map (\v -> v + 10) m)
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.update "a" (Maybe.map (\v -> v + 10))
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.update "a" (Maybe.map ((+) 10))
Dict.fromList [("a",11),("b",2),("c",3)] : Dict.Dict String number
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.remove "a"
Dict.fromList [("b",2),("c",3)] : Dict.Dict String number
--Elm only from here
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.map (\k v -> k ++ toString v)
Dict.fromList [("a","a1"),("b","b2"),("c","c3")] : Dict.Dict String String
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.foldl (\k v z -> v + z) 0
6 : number
> Dict.fromList [("a",1),("b",2),("c",3)] |> Dict.filter (\k v -> v >= 2)
Dict.fromList [("b",2),("c",3)] : Dict.Dict String number
> Dict.union (Dict.fromList [("a",1),("b",2),("c",3)]) (Dict.fromList [("d",4)])
Dict.fromList [("a",1),("b",2),("c",3),("d",4)] : Dict.Dict String number
> Dict.diff (Dict.fromList [("a",1),("b",2),("c",3)]) (Dict.fromList [("a",1)])
Dict.fromList [("b",2),("c",3)] : Dict.Dict String number
For those who are hesitant to get started with functional languages or who are suffering from the gap with object-oriented languages, there is no difference in what they want to do! I wanted you to notice that and summarized it in the article. Also, I wanted you to touch on abundant collections and functions for collection operations as a feature of functional languages, so I briefly arranged the execution examples. I hope you enjoy getting started with functional programming!
Recommended Posts