[JAVA] Une introduction aux types fonctionnels pour les programmeurs orientés objet dans Elm

C'est le quatrième jour du calendrier de l'Avent Elm.

Le troisième jour était @ arowM Gestion des clics spéciaux avec le routage SPA.

J'ai l'impression d'écrire ce genre d'article à l'infini, mais cette fois j'aimerais apprendre les types fonctionnels en touchant les langages orientés objet, mais ceux qui pensent qu'ils sont lourds ou ont commencé à apprendre les types fonctionnels , Cet article s'adresse à ceux qui ne savent pas écrire. Comme un langage orienté objet est trop large, j'ai pris un programme Java comme exemple et je l'ai écrit en sachant comment l'exprimer dans le langage fonctionnel pur Elm.

Class

Dans un langage fonctionnel, la programmation est réalisée en combinant des fonctions et des structures de données. En Java, la programmation se fait en définissant des classes. Si vous ne regardez que cette explication, quelle est la différence entre le type fonctionnel et le langage C? Je me demande si orienté objet et fonctionnel sont des langages complètement différents, mais ce n'est pas le cas. Tout d'abord, préparons un cours simple. Une classe `` Personqui surcharge les champs et certaines méthodes définies dans * Getter *, * Setter * et la classeObject```. Notez que la méthode "toujours" renvoie une valeur. N'est-ce pas * Setter * impossible? Vous pourriez penser qu'il est conçu comme un ** objet immuable ** en renvoyant un nouvel objet avec un nouvel ensemble de valeurs.

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

Maintenant, reproduisons la classe Person '' dans Elm. type Person = Person Name Age`` `` La définition des données elles-mêmes est complétée en une seule ligne. Cela s'appelle * union type *, et définit la définition du champ et le constructeur, et définit `toString```,` `ʻequals```,` hashCode à la fois du côté langage (Elm). Je vais. `` `` Person "Takeshi" 15 Vous pouvez créer une instance (mais pas) en l'écrivant comme ceci. `Name``` et` `ʻAge``` sont juste des alias de type de` StringetInt```, mais le simple fait d'ajouter un alias rend la définition très facile à voir. (Je le veux aussi quand je fais DDD en Java).

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

Dans Elm, une méthode est représentée par une fonction. Qu'est-ce qu'une méthodeUne fonction qui accepte les arguments et l'objet lui-même comme argumentsPeut être considéré comme. L'ordre des arguments ne change pas, maisArgument 1->Argument 2->Argument 3... ->objet->Type de retourIl est recommandé dans Elm de définir dans cet ordre. Le secret est l'opérateur de tuyau(|>)C'est dedans. L'opérateur de tube peut être utilisé de telle sorte que l'argument de la fonction s'écoule du côté gauche vers le côté droit.x |> f == f xLes tubes facilitent l'écriture de code comme des chaînes de méthodes.

-- setName : Name -> Person -> Person
setName "John" (Person "Takeshi" 15 ) == Person "John" 15

-- new Person("Takeshi", 15).setName("John")Vous pouvez le lire de manière fluide!
(Person "Takeshi" 15 |> setName "John") == Person "John" 15

Jetons un coup d'œil aux méthodes (fonctions) de type Personne ''. Les types écrits avec * union type * peuvent être décomposés champ par champ en utilisant une syntaxe appelée correspondance de modèle. Les champs inutilisés peuvent être ignorés avec _```. La syntaxe `let in `définit une variable locale dans la clause let et écrit une expression qui renvoie la valeur de retour finale dans 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

La syntaxe peut être un peu ennuyeuse, mais est-ce que c'est? Ce que vous faites à la fois en POO et en PF est le même, non? Avez-vous envie de dire ça? C'est la forme de base de la pensée. De plus, pour les structures de données telles que Person, * Getter *, * Setter * peuvent être écrits plus simplement en utilisant la syntaxe d'enregistrement prédéfinie. La méthode d'écriture est similaire à Json, et elle est accessible sous la forme de `` .field '', donc la méthode d'écriture est plus proche de la méthode d'écriture orientée objet. Il existe également une syntaxe de correspondance de modèle pour les enregistrements.

type alias Person = { name : Name, age : Age }

--Génération d'enregistrement
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

Une fois que vous avez défini la classe, reproduisons la composition. Voici l'exemple de code en Java. C'est lourd quand j'essaye de l'écrire correctement (je n'utilise pas une bibliothèque comme Lombok pour que tout le monde puisse la comprendre).

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

C'est fondamentalement le même que le contenu expliqué dans Class. Apportez simplement le type `` Bar '' sur le terrain. La correspondance de modèle peut décomposer la structure sans aucun problème, même si * union type * est imbriqué.

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

La structure peut être gérée de manière transparente en utilisant la correspondance de motifs, et le type `` Barre '' sur le côté à composer n'a aucune fonction. Bien entendu, si la structure et l'utilisation sont compliquées, il vaut mieux partager la responsabilité de la fonction pour chaque module comme dans l'exemple Java. Il est également possible de coder en pensant à l'encapsulation. Vous pouvez imbriquer des enregistrements comme décrit dans la classe ci-dessus (bien que la mise à jour soit un peu plus compliquée). La liberté d'écrire ici est exactement la même que Java (j'ai été surpris quand j'ai découvert que je pouvais le faire avec le même sentiment).

module Bar exposing (..)


type Bar
    = Bar Int Int

Polymophism

Je pense que j'ai pu comprendre comment créer des classes de style Elm et comment utiliser des modules via Composition. Bien sûr, je ne pense pas être satisfait de cela seul. Reproduisons le polymorphisme en utilisant l'interface avec Elm. Utilisons les modèles Tree et Visitor, qui effectuent des calculs simples, comme exemple de code. Pour simplifier l'exemple, la formule ne prend en charge que les nombres et les ajouts.

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 est vraiment bon dans le code qui utilise pleinement le polymorphisme tel que le modèle Visitor. Le * union type * que j'ai utilisé jusqu'à présent, mais en utilisant la somme directe des types, il montre son vrai caractère. `type Node = Num Int | Add Node Node``` Le type` Node est composé de deux types, `` `Num et ʻAdd, et plus encore`. `ʻAdd a une structure récursive avec deux de ses propres types``Node. Le formulaire lui-même est le même que le type Java `` Node '', mais il est utilisé différemment. Jetons un coup d'œil à la fonction `` calc ''. Vous pouvez voir la nouvelle syntaxe `` `` case node of` ''. Il s'agit d'un type de correspondance de modèle et est écrit lorsqu'il est utilisé comme expression plutôt que comme argument. Il est utilisé pour classer le type de somme directe de * type d'union *. D'un autre point de vue, on peut voir que le type `` `Num et le type ʻAdd implémentent une interface avec la méthode` `` calc. De plus, dans le cas du type Add, vous pouvez voir que calc``` est appelé récursivement pour trouver les valeurs sur les côtés droit et gauche. L'arbre peut être représenté intuitivement par * 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

Cela peut prendre un certain temps pour s'y habituer, mais en gros, vous pouvez facilement reproduire ce que vous faisiez orienté objet avec une fonction qui utilise * union type * + pattern matching. Il existe de nombreux cas où vous pouvez écrire de manière très concise, alors maîtrisons-le.

Collection

Comparons les collections utilisant jshell et Java9. Les résultats de l'exécution sont également affichés, alors sentez la différence.

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}
//A partir de là, c'est trop glissant et j'abandonne

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 seulement d'ici
> 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

Résumé

Pour ceux qui hésitent à se lancer dans les langages fonctionnels ou qui souffrent du décalage avec les langages orientés objet, il n'y a pas de différence dans ce qu'ils veulent faire! Je voulais que vous le remarquiez et que vous le résumiez dans l'article. De plus, je voulais que vous abordiez les abondantes collections et fonctions pour les opérations de collecte en tant que caractéristique du langage fonctionnel, alors j'ai brièvement organisé les exemples d'exécution. J'espère que vous apprécierez de commencer avec le type fonctionnel!

Recommended Posts

Une introduction aux types fonctionnels pour les programmeurs orientés objet dans Elm
Introduction à la programmation fonctionnelle (Java, Javascript)
Introduction à la programmation pour les étudiants du Collège: Introduction
Introduction à la programmation pour les étudiants du collégial: variables
Premiers pas avec Groovy pour les ingénieurs Java gênants
Introduction à la programmation pratique
Introduction à la programmation pour les étudiants du collégial (mise à jour de temps à autre)
Introduction à la programmation pour les étudiants du Collège: création de canevas
~ J'ai essayé d'apprendre la programmation fonctionnelle avec Java maintenant ~
Qu'est-ce que l'orientation objet après tout ou juste une chose à surveiller dans la programmation
Introduction à la programmation pour les étudiants du Collège: Préparation Installons le traitement
Introduction à la programmation pour les étudiants: comment dessiner des carrés de base
Introduction à la programmation pour les étudiants du Collège: dessiner des lignes droites
Introduction à la programmation pour les étudiants du collège: rendre les lignes droites plus attrayantes
Comprendre comment la programmation fonctionnelle a été introduite dans Java d'un seul coup
Introduction à la programmation pour les étudiants du collégial: diverses fonctions liées aux carrés (partie 1)
Comment maîtriser la programmation en 3 mois
Introduction à kotlin pour les développeurs iOS ⑥ ー création de kotlin
Introduction à kotlin pour les développeurs iOS ④-type
Développement piloté par les tests avec le langage fonctionnel Elm
[Pour les débutants] Comment déboguer avec Eclipse
Pour mon fils qui a commencé à étudier Java avec "Introduction à Java" dans une main
Comment faire une méthode de jugement pour rechercher n'importe quel caractère dans le tableau