Dies ist der vierte Tag des Ulmen-Adventskalenders.
Der dritte Tag war @ arowMs Umgang mit speziellen Klicks mit SPA-Routing.
Ich habe das Gefühl, dass ich unendlich viele Artikel wie diesen schreibe, aber dieses Mal möchte ich Funktionstypen lernen, indem ich objektorientierte Sprachen berühre, aber diejenigen, die denken, dass sie schwer sind oder angefangen haben, Funktionstypen zu lernen. Dieser Artikel ist für diejenigen, die nicht schreiben können. Da eine objektorientierte Sprache zu weit gefasst ist, habe ich ein Java-Programm als Beispiel genommen und es mit dem Bewusstsein geschrieben, wie man es in der reinen Funktionssprache Elm ausdrückt.
Class
In einer funktionalen Sprache erfolgt die Programmierung durch Kombinieren von Funktionen und Datenstrukturen. In Java erfolgt die Programmierung durch Definieren von Klassen. Wenn Sie sich nur diese Erklärung ansehen, was ist der Unterschied zwischen dem Funktionstyp und der C-Sprache? Ich frage mich, ob objektorientiert und funktional völlig unterschiedliche Sprachen sind, aber das ist nicht der Fall. Lassen Sie uns zunächst eine einfache Klasse vorbereiten. Eine `Person``` Klasse, die Felder und einige Methoden überlädt, die in * Getter *, * Setter * und der`
Object``` Klasse definiert sind. Beachten Sie, dass die Methode "always" einen Wert zurückgibt. Ist * Setter * nicht unmöglich? Sie könnten denken, dass es als ** unveränderliches Objekt ** konzipiert ist, indem Sie ein neues Objekt mit einem neuen Wertesatz zurückgeben.
// 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;
}
}
Lassen Sie uns nun die `Person``` Klasse in Elm reproduzieren.
Typ Person = Personenname Alter`` `` Die Definition der Daten selbst wird in nur einer Zeile abgeschlossen. Dies wird als * Vereinigungstyp * bezeichnet und definiert die Felddefinition und den Konstruktor sowie ``
toString,` `` equals
, hashCode
auf einmal auf der Sprachseite (Elm). Ich werde. `Person" Takeshi "15``` Sie können eine Instanz erstellen (obwohl nicht), indem Sie sie so schreiben.
Name``` und
Age``` sind nur Alias vom Typ
String``` und
`` Int```, aber nur das Hinzufügen von Alias macht die Definition sehr einfach zu sehen. (Ich möchte es, wenn ich DDD auch in Java mache).
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 wird eine Methode durch eine Funktion dargestellt. Was ist eine Methode?Eine Funktion, die Argumente und das Objekt selbst als Argumente akzeptiertKann als betrachtet werden. Die Reihenfolge der Argumente ändert sich aber nichtArgument 1->Argument 2->Argument 3... ->Objekt->Rückgabetyp
In Elm wird empfohlen, in dieser Reihenfolge zu definieren. Das Geheimnis ist der Rohrbetreiber(|>)Es ist in. Der Pipe-Operator kann so verwendet werden, dass das Argument der Funktion von der linken zur rechten Seite fließt.x |> f == f x
Pipes erleichtern das Schreiben von Code wie Methodenketten.
-- setName : Name -> Person -> Person
setName "John" (Person "Takeshi" 15 ) == Person "John" 15
-- new Person("Takeshi", 15).setName("John")Sie können es fließend lesen!
(Person "Takeshi" 15 |> setName "John") == Person "John" 15
Werfen wir einen Blick auf die Methoden (Funktionen) vom Typ `Person```. Mit * union type * geschriebene Typen können mithilfe einer Syntax namens Pattern Matching Feld für Feld zerlegt werden. Nicht verwendete Felder können mit
_``` ignoriert werden. Die Syntax ``
let in`` `definiert eine lokale Variable in der let-Klausel und schreibt einen Ausdruck, der den endgültigen Rückgabewert in in zurückgibt.
{-| (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
Die Syntax mag etwas nervig sein, aber ist das so? Was Sie sowohl in OOP als auch in FP tun, ist dasselbe, richtig? Hast du Lust das zu sagen? Dies ist die Grundform des Denkens. Für Datenstrukturen wie Person können * Getter *, * Setter * einfacher mithilfe der vordefinierten Datensatzsyntax geschrieben werden. Die Schreibmethode ähnelt Json und kann in Form von ".field" aufgerufen werden, sodass die Schreibmethode näher an der objektorientierten Schreibmethode liegt. Es gibt auch eine Musterübereinstimmungssyntax für Datensätze.
type alias Person = { name : Name, age : Age }
--Datensatzgenerierung
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
Nachdem Sie die Klasse definiert haben, reproduzieren wir die Komposition. Hier ist der Beispielcode in Java. Es ist schwer, wenn ich versuche, es richtig zu schreiben (ich benutze keine Bibliothek wie Lombok, damit jeder es verstehen kann).
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;
}
}
Dies entspricht im Wesentlichen dem in der Klasse erläuterten Inhalt. Bringen Sie einfach den Typ "Bar" auf das Feld. Die Musterübereinstimmung kann die Struktur problemlos zerlegen, selbst wenn * Vereinigungstyp * verschachtelt ist.
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
Die Struktur kann durch Mustervergleich transparent gehandhabt werden, und der Typ "Balken" auf der zu komponierenden Seite hat keine Funktion. Wenn die Struktur und Verwendung kompliziert sind, ist es natürlich besser, die Verantwortung für die Funktion für jedes Modul wie im Java-Beispiel zu teilen. Es ist auch möglich, unter Berücksichtigung der Kapselung zu codieren. Sie können Datensätze wie oben in Klasse beschrieben verschachteln (obwohl das Aktualisieren etwas komplizierter ist). Die Freiheit, hier herum zu schreiben, ist genau die gleiche wie bei Java (ich war überrascht, als ich herausfand, dass ich es mit dem gleichen Gefühl tun konnte).
module Bar exposing (..)
type Bar
= Bar Int Int
Polymophism
Ich glaube, ich konnte verstehen, wie man Klassen im Elm-Stil erstellt und wie man Module durch Komposition verwendet. Natürlich glaube ich nicht, dass ich damit allein zufrieden bin. Lassen Sie uns den Polymorphismus über die Schnittstelle mit Elm reproduzieren. Verwenden wir als Beispielcode die Baum- und Besuchermuster, die einfache Berechnungen durchführen. Um das Beispiel zu vereinfachen, unterstützt die Formel nur Zahlen und Additionen.
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 ist wirklich gut in Code, der Polymorphismus wie das Besuchermuster voll ausnutzt. Der * Union-Typ *, den ich bisher verwendet habe, aber durch die Verwendung der direkten Summe der Typen zeigt er seinen wahren Charakter. `Typ Node = Num Int | Add Node Node
`Node``` Typ besteht aus zwei Typen,
Num``` und
Add``` und weiter
Add``` hat eine rekursive Struktur mit zwei eigenen
Node```-Typen. Das Formular selbst ist das gleiche wie der Java-Typ "Node", wird jedoch anders verwendet. Werfen wir einen Blick auf die Funktion ``
calc. Sie können die neue Syntax `` `case node of`` `sehen. Dies ist eine Art Musterübereinstimmung und wird geschrieben, wenn sie eher als Ausdruck als als Argument verwendet wird. Es wird verwendet, um den direkten Summentyp * Vereinigungstyp * zu klassifizieren. Aus einem anderen Blickwinkel ist ersichtlich, dass der Typ `` `Num
und der Typ`` Add``` eine Schnittstelle mit der Methode ``
calc``` implementieren. Außerdem können Sie im Fall des Typs "Hinzufügen" sehen, dass "calc" rekursiv aufgerufen wird, um die Werte auf der rechten und linken Seite zu finden. Der Baum kann intuitiv durch * Vereinigungstyp * dargestellt werden.
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
Es mag etwas gewöhnungsbedürftig sein, aber im Grunde können Sie mit einer Funktion, die * Vereinigungstyp * + Mustervergleich verwendet, problemlos reproduzieren, was Sie objektorientiert getan haben. Es gibt viele Fälle, in denen Sie sehr präzise schreiben können. Lassen Sie es uns also beherrschen.
Collection
Vergleichen wir die Sammlungen mit jshell und Java9. Die Ausführungsergebnisse werden ebenfalls angezeigt. Bitte spüren Sie den Unterschied.
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}
//Ab diesem Zeitpunkt aufgegeben
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
--Ulme nur von hier
> 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
Für diejenigen, die zögern, mit funktionalen Sprachen zu beginnen, oder die unter der Lücke zu objektorientierten Sprachen leiden, gibt es keinen Unterschied, was sie tun möchten! Ich wollte, dass Sie das bemerken und fasste es im Artikel zusammen. Außerdem wollte ich, dass Sie die zahlreichen Sammlungen und Funktionen für Sammlungsvorgänge als Merkmal der Funktionssprache ansprechen, und habe daher die Ausführungsbeispiele kurz angeordnet. Ich wünsche Ihnen viel Spaß beim Einstieg in den Funktionstyp!
Recommended Posts