An introduction to functional programming for object-oriented programmers in Elm

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 theObject``` 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.

Person.java

// 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. `` `NameandAgeare just type aliases of StringandInt```, but just adding alias makes the definition very easy to see. (I want it when I do DDD in Java too).

Person.elm

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 typeElm 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 xPipes 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).

Foo.java

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 + ')';
    }
}

Bar.java

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.

NodeTest.java

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()));
    }
}

Node.java

package polymorphism;

public interface Node {
    int accept(Visitor visitor);
}

NumNode.java


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);
    }
}

AddNode.java

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);
    }
}

Visitor.java

package polymorphism;

public interface Visitor {
    int visit(NumNode node);

    int visit(AddNode node);
}

Calculator.java

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 *.

Node.elm

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

Summary

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

An introduction to functional programming for object-oriented programmers in Elm
Introduction to Functional Programming (Java, Javascript)
Introduction to Programming for College Students: Introduction
Introduction to Programming for College Students: Variables
An introduction to Groovy for tedious Java engineers
Introduction to Practical Programming
Introduction to programming for college students (updated from time to time)
Introduction to Programming for College Students: Making a Canvas
~ I tried to learn functional programming in Java now ~
What is object-oriented after all or just one thing to watch out for in programming
Introduction to Programming for College Students: Preparation Let's Install Processing
Introduction to Programming for College Students: How to Draw Basic Rectangle
Introduction to Programming for College Students: Draw a Straight Line
Introduction to Programming for College Students: Make Straight Lines More Attractive
Understand how functional programming was introduced to Java in one shot
Introduction to Programming for College Students: Various Functions Related to Rectangle (Part 1)
How to master programming in 3 months
Introduction to kotlin for iOS developers ⑥-Kotlin creation
Introduction to kotlin for iOS developers ④-Type
Test-driven development in the functional language Elm
[For beginners] How to debug in Eclipse
For my son who started studying Java with "Introduction to Java" in one hand
How to make a judgment method to search for an arbitrary character in an array