)) } } }

Scala - Opcja Monad / Może Monad

import language.higherKinds trait Monad[M[_]] { def pure[A](a: A): M[A] def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B] def map[A, B](ma: M[A])(f: A => B): M[B] = flatMap(ma)(x => pure(f(x))) } object Monad { def apply[F[_]](implicit M: Monad[F]): Monad[F] = M implicit val myOptionMonad = new Monad[MyOption] { def pure[A](a: A) = MySome(a) def flatMap[A, B](ma: MyOption[A])(f: A => MyOption[B]): MyOption[B] = ma match { case MyNone => MyNone case MySome(a) => f(a) } } } sealed trait MyOption[+A] { def flatMap[B](f: A => MyOption[B]): MyOption[B] = Monad[MyOption].flatMap(this)(f) def map[B](f: A => B): MyOption[B] = Monad[MyOption].map(this)(f) } case object MyNone extends MyOption[Nothing] case class MySome[A](x: A) extends MyOption[A]

Zaczynamy od implementacji Monad klasa, która będzie podstawą dla wszystkich naszych implementacji monad. Posiadanie tej klasy jest bardzo przydatne, ponieważ implementując tylko dwie jej metody - pure oraz flatMap —dla konkretnej monady, otrzymasz wiele metod za darmo (w naszych przykładach ograniczamy się do metody map, ale ogólnie istnieje wiele innych przydatnych metod, takich jak sequence i traverse do pracy z tablicami Monad s).

Możemy wyrazić map jako kompozycja pure i flatMap. Na podstawie podpisu $ flatMap: (T to M [U]) to (M [T] to M [U]) $ flatMap widać, że jest naprawdę blisko $ map: (T do U) do (M [T] do M [U]) $. Różnica polega na dodatkowym $ M $ w środku, ale możemy użyć pure funkcja do konwersji $ U $ na $ M [U] $. W ten sposób wyrażamy map pod względem flatMap i pure.

Działa to dobrze w przypadku Scali, ponieważ ma zaawansowany system typów. Działa również dobrze w przypadku JS, Python i Ruby, ponieważ są one wpisywane dynamicznie. Niestety nie działa w przypadku Swift, ponieważ jest on wpisywany statycznie i nie ma zaawansowanych funkcji pisania, takich jak typy wyższego rzędu , więc dla Swift będziemy musieli zaimplementować map dla każdej monady.

Zwróć również uwagę, że monada Option to już plik de facto standard dla języków takich jak Swift i Scala, więc używamy nieco innych nazw dla naszych implementacji monad.

Teraz, gdy mamy bazę Monad przejdźmy do naszych implementacji monad Option. Jak wspomniano wcześniej, podstawową ideą jest to, że Option albo posiada jakąś wartość (nazywaną Some) albo w ogóle nie posiada żadnej wartości (None).

pure po prostu promuje wartość do Some, podczas gdy flatMap Metoda sprawdza aktualną wartość Option - jeśli tak jest None to zwraca None, a jeśli to Some z wartością bazową, wyodrębnia wartość bazową, stosuje f() do niego i zwraca wynik.

Zauważ, że po prostu używając tych dwóch funkcji i map, nie można dostać się do zerowego wyjątku wskaźnika - nigdy. (Problem mógłby potencjalnie powstają w naszej implementacji flatMap ale to tylko kilka wierszy w naszym kodzie, które sprawdzamy raz. Potem po prostu używamy naszej implementacji monady Option w całym naszym kodzie w tysiącach miejsc i nie musimy się wcale obawiać wyjątku pustego wskaźnika).

Każda monada

Przejdźmy do drugiej monady: albo. Jest to w zasadzie to samo, co monada Option, ale z Some o nazwie Right i None o nazwie Left. Ale tym razem Left może mieć również wartość bazową.

Potrzebujemy tego, ponieważ bardzo wygodnie jest wyrazić zgłoszenie wyjątku. Jeśli wystąpił wyjątek, to wartość Either będzie Left(Exception). flatMap funkcja nie postępuje, jeśli wartością jest Left, co powtarza semantykę wyrzucania wyjątków: jeśli wystąpił wyjątek, zatrzymujemy dalsze wykonywanie.

strojenie wydajności bazy danych serwera sql

JavaScript - albo Monada

import Monad from './monad'; export class Either extends Monad { // pure :: a -> Either a pure = (value) => { return new Right(value) } // flatMap :: # Either a -> (a -> Either b) -> Either b flatMap = f => this.isLeft() ? this : f(this.value) isLeft = () => this.constructor.name === 'Left' } export class Left extends Either { constructor(value) { super(); this.value = value; } toString() { return `Left(${this.value})` } } export class Right extends Either { constructor(value) { super(); this.value = value; } toString() { return `Right(${this.value})` } } // attempt :: (() -> a) -> M a Either.attempt = f => { try { return new Right(f()) } catch(e) { return new Left(e) } } Either.pure = (new Left(null)).pure

Python - albo Monada

from monad import Monad class Either(Monad): # pure :: a -> Either a @staticmethod def pure(value): return Right(value) # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(self, f): if self.is_left: return self else: return f(self.value) class Left(Either): def __init__(self, value): self.value = value self.is_left = True class Right(Either): def __init__(self, value): self.value = value self.is_left = False

Ruby - albo Monada

require_relative './monad' class Either Either a def self.pure(value) Right.new(value) end # pure :: a -> Either a def pure(value) self.class.pure(value) end # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(f) if is_left self else f.call(value) end end end class Left

Szybki - albo Monada

import Foundation enum Either { case Left(A) case Right(B) static func pure(_ value: C) -> Either { return Either.Right(value) } func flatMap(_ f: (B) -> Either) -> Either { switch self { case .Left(let x): return Either.Left(x) case .Right(let x): return f(x) } } func map(f: (B) -> C) -> Either { return self.flatMap { Either.pure(f(

Opcja / Może, Albo i Przyszłe Monady w JavaScript, Pythonie, Ruby, Swift i Scali

Ten samouczek monad zawiera krótkie wyjaśnienie monad i pokazuje, jak zaimplementować najbardziej przydatne w pięciu różnych językach programowania - jeśli szukasz monad w JavaScript , monady w Pyton , monady w Rubin , monady w Szybki i / lub monady w formacie Drabina lub aby porównać dowolne implementacje, czytasz właściwy artykuł!

Korzystając z tych monad, pozbędziesz się szeregu błędów, takich jak wyjątki pustego wskaźnika, nieobsłużone wyjątki i warunki wyścigu.



Oto, co omówię poniżej:



  • Wprowadzenie do teorii kategorii
  • Definicja monady
  • Implementacje monady Option („Może”), monady lub monady Future oraz przykładowego programu wykorzystującego je w JavaScript, Python, Ruby, Swift i Scala

Zacznijmy! Naszym pierwszym przystankiem jest teoria kategorii, która jest podstawą monad.

Wprowadzenie do teorii kategorii

Teoria kategorii to dziedzina matematyczna aktywnie rozwijana w połowie XX wieku. Teraz jest podstawą wielu koncepcji programowania funkcjonalnego, w tym monady. Przyjrzyjmy się szybko niektórym koncepcjom teorii kategorii, dostosowanym do terminologii tworzenia oprogramowania.



Istnieją więc trzy podstawowe pojęcia, które definiują plik Kategoria:

  1. Rodzaj jest dokładnie taki, jaki widzimy w językach z typami statycznymi. Przykłady: Int, String, Dog, Cat itd.
  2. Funkcje połącz dwa typy. Dlatego mogą być przedstawiane jako strzałki od jednego typu do innego typu lub do siebie. Funkcję $ f $ od typu $ T $ do typu $ U $ można oznaczyć jako $ f: T do U $. Można o tym myśleć jako o funkcji języka programowania, która przyjmuje argument typu $ T $ i zwraca wartość typu $ U $.
  3. Kompozycja jest operacją, oznaczoną operatorem $ cdot $, która buduje nowe funkcje z już istniejących. W kategorii zawsze gwarantuje się, że dla dowolnych funkcji $ f: T do U $ i $ g: U do V $ istnieje unikalna funkcja $ h: T do V $. Ta funkcja jest oznaczona jako $ f cdot g $. Operacja skutecznie odwzorowuje parę funkcji na inną funkcję. W językach programowania ta operacja jest oczywiście zawsze możliwa. Na przykład, jeśli masz funkcję zwracającą długość łańcucha - $ strlen: String to Int $ - oraz funkcję, która mówi, czy liczba jest parzysta - $ even: Int to Boolean $ - wtedy możesz utworzyć function $ even { _} strlen: String to Boolean $, który mówi, czy długość String jest równa. W tym przypadku $ even { _} strlen = even cdot strlen $. Skład implikuje dwie cechy:
    1. Kojarzenie: $ f cdot g cdot h = (f cdot g) cdot h = f cdot (g cdot h) $
    2. Istnienie funkcji tożsamości: $ forall T: istnieje f: T do T $ lub w prostym języku angielskim, dla każdego typu $ T $ istnieje funkcja, która odwzorowuje $ T $ na siebie.

Przyjrzyjmy się więc prostej kategorii.

Prosta kategoria obejmująca String, Int i Double oraz niektóre funkcje wśród nich.



Uwaga dodatkowa: Zakładamy, że Int, String a wszystkie inne typy tutaj nie mają wartości null, tj. wartość null nie istnieje.

Uwaga dodatkowa 2: to jest właściwie tylko część kategorii, ale to wszystko, czego chcemy w naszej dyskusji, ponieważ zawiera wszystkie niezbędne części, których potrzebujemy, a dzięki temu diagram jest mniej zagracony. Prawdziwa kategoria miałaby również wszystkie złożone funkcje, takie jak $ roundToString: Double to String = intToString cdot round $, aby spełnić klauzulę składu kategorii.

Możesz zauważyć, że funkcje w tej kategorii są bardzo proste. W rzeczywistości jest prawie niemożliwe, aby mieć błąd w tych funkcjach. Nie ma żadnych wartości null, żadnych wyjątków, tylko arytmetyka i praca z pamięcią. Zatem jedyną złą rzeczą, jaka może się zdarzyć, jest awaria procesora lub pamięci - w takim przypadku i tak trzeba zawiesić program - ale zdarza się to bardzo rzadko.

Czy nie byłoby miło, gdyby cały nasz kod po prostu działał na tym poziomie stabilności? Absolutnie! Ale co na przykład z I / O? Na pewno nie możemy bez tego żyć. Oto, gdzie z pomocą przychodzą rozwiązania monad: izolują wszystkie niestabilne operacje w bardzo małe i bardzo dobrze sprawdzone fragmenty kodu - wtedy możesz używać stabilnych obliczeń w całej aplikacji!

Wejdź do Monady

Nazwijmy niestabilne zachowanie, takie jak I / O a efekt uboczny . Teraz chcemy móc pracować ze wszystkimi naszymi poprzednio zdefiniowanymi funkcjami, takimi jak length i typy takie jak String w stabilny sposób w obecności tego efekt uboczny .

Zacznijmy więc od pustej kategorii $ M [A] $ i przejdźmy do kategorii, która będzie miała wartości z jednym konkretnym typem efektu ubocznego, a także wartości bez skutków ubocznych. Załóżmy, że zdefiniowaliśmy tę kategorię i jest ona pusta. W tej chwili nie możemy z nim nic zrobić, więc aby uczynić go użytecznym, wykonamy te trzy kroki:

  1. Wypełnij go wartościami typów z kategorii $ A $, np. String, Int, Double Itd. (Zielone pola na poniższym diagramie)
  2. Kiedy już mamy te wartości, nadal nie możemy z nimi zrobić nic sensownego, więc potrzebujemy sposobu na pobranie każdej funkcji $ f: T do U $ z $ A $ i utworzenie funkcji $ g: M [T] do M [U] $ (niebieskie strzałki na poniższym diagramie). Kiedy już mamy te funkcje, możemy zrobić wszystko z wartościami w kategorii $ M [A] $, które byliśmy w stanie zrobić w kategorii $ A $.
  3. Teraz, gdy mamy zupełnie nową kategorię $ M [A] $, pojawia się nowa klasa funkcji z sygnaturą $ h: T do M [U] $ (czerwone strzałki na poniższym diagramie). Pojawiają się w wyniku promowania wartości w kroku pierwszym jako część naszego kodu, tj. Piszemy je w razie potrzeby; to są główne rzeczy, które odróżniają pracę z $ M [A] $ od pracy z $ A $. Ostatnim krokiem będzie sprawienie, aby te funkcje działały dobrze również na typach w $ M [A] $, tj. Można było wyprowadzić funkcję $ m: M [T] do M [U] $ z $ h: T do M [U] $

Tworzenie nowej kategorii: Kategorie A i M [A] oraz czerwona strzałka od A

Zacznijmy więc od zdefiniowania dwóch sposobów promowania wartości typów $ A $ do wartości typów $ M [A] $: jednej funkcji bez skutków ubocznych i jednej z efektami ubocznymi.

  1. Pierwsza nazywa się $ pure $ i jest definiowana dla każdej wartości stabilnej kategorii: $ pure: T do M [T] $. Wynikowe wartości $ M [T] $ nie będą miały żadnych skutków ubocznych, dlatego ta funkcja nazywa się $ pure $. Np. Dla monady I / O $ pure $ zwróci pewną wartość natychmiast, bez możliwości awarii.
  2. Drugi nazywa się $ konstruktor $ i, w przeciwieństwie do $ pure $, zwraca $ M [T] $ z pewnymi skutkami ubocznymi. Przykładem takiego konstruktora $ $ dla asynchronicznej monady we / wy może być funkcja, która pobiera dane z sieci i zwraca je jako String. Wartość zwrócona przez $ konstruktora $ będzie miała w tym przypadku typ $ M [String] $.

Teraz, gdy mamy dwa sposoby promowania wartości w $ M [A] $, do Ciebie jako programisty należy wybór funkcji, której chcesz użyć, w zależności od celów programu. Rozważmy tutaj przykład: Chcesz pobrać stronę HTML, taką jak https://www.toptal.com/javascript/option-maybe-either-future-monads-js, i w tym celu tworzysz funkcję $ fetch $. Ponieważ wszystko może pójść nie tak podczas pobierania - pomyśl o awariach sieci itp. - użyjesz $ M [String] $ jako typu zwracanego przez tę funkcję. Więc będzie to wyglądało jak $ fetch: String do M [String] $ i gdzieś w ciele funkcji użyjemy konstruktora $ $ dla $ M $.

Teraz załóżmy, że wykonujemy pozorowaną funkcję do testowania: $ fetchMock: String do M [String] $. Nadal ma ten sam podpis, ale tym razem po prostu wstrzykujemy wynikową stronę HTML do treści $ fetchMock $ bez wykonywania żadnych niestabilnych operacji sieciowych. W tym przypadku używamy po prostu $ pure $ w implementacji $ fetchMock $.

W następnym kroku potrzebujemy funkcji, która bezpiecznie promuje dowolną funkcję $ f $ z kategorii $ A $ do $ M [A] $ (niebieskie strzałki na diagramie). Ta funkcja nazywa się $ map: (T to U) to (M [T] to M [U]) $.

Teraz mamy kategorię (która może mieć skutki uboczne, jeśli użyjemy $ konstruktora $), która również zawiera wszystkie funkcje z kategorii stabilnej, co oznacza, że ​​są one również stabilne w $ M [A] $. Możesz zauważyć, że jawnie wprowadziliśmy inną klasę funkcji, takich jak $ f: T do M [U] $. Np. $ Pure $ i $ konstruktor $ są przykładami takich funkcji dla $ U = T $, ale oczywiście może ich być więcej, jak gdybyśmy mieli użyć $ pure $, a następnie $ map $. Zatem generalnie potrzebujemy sposobu radzenia sobie z dowolnymi funkcjami w postaci $ f: T do M [U] $.

Jeśli chcemy stworzyć nową funkcję opartą na $ f $, którą można by zastosować do $ M [T] $, możemy spróbować użyć $ map $. Ale to doprowadzi nas do funkcji $ g: M [T] do M [M [U]] $, co nie jest dobre, ponieważ nie chcemy mieć jeszcze jednej kategorii $ M [M [A]] $. Aby poradzić sobie z tym problemem, wprowadzamy ostatnią funkcję: $ flatMap: (T to M [U]) to (M [T] to M [U]) $.

Ale dlaczego mielibyśmy to robić? Załóżmy, że jesteśmy po kroku 2, czyli mamy $ pure $, $ constructor $ i $ map $. Powiedzmy, że chcemy pobrać stronę HTML z toptal.com, a następnie zeskanować wszystkie adresy URL i pobrać je. Zrobiłbym funkcję $ fetch: String do M [String] $, która pobiera tylko jeden adres URL i zwraca stronę HTML.

Następnie zastosowałbym tę funkcję do adresu URL i uzyskałbym stronę z toptal.com, czyli $ x: M [String] $. Teraz robię transformację na $ x $ i ostatecznie dochodzę do jakiegoś adresu URL $ u: M [String] $. Chcę zastosować do niego funkcję $ fetch $, ale nie mogę, ponieważ przyjmuje typ $ String $, a nie $ M [String] $. Dlatego potrzebujemy $ flatMap $, aby przekonwertować $ fetch: String na M [String] $ na $ m_fetch: M [String] na M [String] $.

Teraz, gdy wykonaliśmy wszystkie trzy kroki, możemy właściwie skomponować dowolne transformacje wartości, których potrzebujemy. Na przykład, jeśli masz wartość $ x $ typu $ M [T] $ i $ f: T do U $, możesz użyć $ map $, aby zastosować $ f $ do wartości $ x $ i otrzymać wartość $ y $ typu $ M [U] $. W ten sposób każda transformacja wartości może być wykonana w 100% bez błędów, o ile implementacje $ pure $, $ constructor $, $ map $ i $ flatMap $ są wolne od błędów.

Dlatego zamiast zajmować się nieprzyjemnymi efektami za każdym razem, gdy napotkasz je w swojej bazie kodu, wystarczy upewnić się, że tylko te cztery funkcje są poprawnie zaimplementowane. Na końcu programu otrzymasz tylko jeden $ M [X] $, w którym możesz bezpiecznie rozpakować wartość $ X $ i obsłużyć wszystkie przypadki błędów.

Tym właśnie jest monada: czymś, co implementuje $ pure $, $ map $ i $ flatMap $. (Właściwie $ map $ można wyprowadzić z $ pure $ i $ flatMap $, ale jest to bardzo użyteczna i rozpowszechniona funkcja, więc nie pominęłam go w definicji).

Option Monad, a.k.a. The Maybe Monad

OK, przejdźmy do praktycznej implementacji i użycia monad. Pierwszą naprawdę pomocną monadą jest monada Option. Jeśli korzystasz z klasycznych języków programowania, prawdopodobnie spotkałeś się z wieloma awariami z powodu niesławnego błędu pustego wskaźnika. Tony Hoare, wynalazca null, nazywa ten wynalazek „błędem za miliard dolarów”:

Doprowadziło to do niezliczonych błędów, luk w zabezpieczeniach i awarii systemu, które prawdopodobnie spowodowały miliardy dolarów bólu i szkód w ciągu ostatnich czterdziestu lat.

Spróbujmy więc to poprawić. Monada opcji albo przechowuje jakąś inną wartość niż null, albo nie ma żadnej wartości. Całkiem podobny do wartości null, ale mając tę ​​monadę, możemy bezpiecznie używać naszych dobrze zdefiniowanych funkcji bez obawy o wyjątek wskaźnika zerowego. Przyjrzyjmy się implementacjom w różnych językach:

JavaScript - opcja Monad / Maybe Monad

class Monad { // pure :: a -> M a pure = () => { throw 'pure method needs to be implemented' } // flatMap :: # M a -> (a -> M b) -> M b flatMap = (x) => { throw 'flatMap method needs to be implemented' } // map :: # M a -> (a -> b) -> M b map = f => this.flatMap(x => new this.pure(f(x))) } export class Option extends Monad { // pure :: a -> Option a pure = (value) => { if ((value === null) || (value === undefined)) { return none; } return new Some(value) } // flatMap :: # Option a -> (a -> Option b) -> Option b flatMap = f => this.constructor.name === 'None' ? none : f(this.value) // equals :: # M a -> M a -> boolean equals = (x) => this.toString() === x.toString() } class None extends Option { toString() { return 'None'; } } // Cached None class value export const none = new None() Option.pure = none.pure export class Some extends Option { constructor(value) { super(); this.value = value; } toString() { return `Some(${this.value})` } }

Python - opcja Monad / Maybe Monad

class Monad: # pure :: a -> M a @staticmethod def pure(x): raise Exception('pure method needs to be implemented') # flat_map :: # M a -> (a -> M b) -> M b def flat_map(self, f): raise Exception('flat_map method needs to be implemented') # map :: # M a -> (a -> b) -> M b def map(self, f): return self.flat_map(lambda x: self.pure(f(x))) class Option(Monad): # pure :: a -> Option a @staticmethod def pure(x): return Some(x) # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(self, f): if self.defined: return f(self.value) else: return nil class Some(Option): def __init__(self, value): self.value = value self.defined = True class Nil(Option): def __init__(self): self.value = None self.defined = False nil = Nil()

Ruby - opcja monada / może monada

class Monad # pure :: a -> M a def self.pure(x) raise StandardError('pure method needs to be implemented') end # pure :: a -> M a def pure(x) self.class.pure(x) end def flat_map(f) raise StandardError('flat_map method needs to be implemented') end # map :: # M a -> (a -> b) -> M b def map(f) flat_map(-> (x) { pure(f.call(x)) }) end end class Option Option a def self.pure(x) Some.new(x) end # pure :: a -> Option a def pure(x) Some.new(x) end # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(f) if defined f.call(value) else $none end end end class Some