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 classe
Object```. 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.
// 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`
Stringet
Int```, 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).
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 retour
Il 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 x
Les 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).
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;
}
}
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.
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 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 *.
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
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