Kiedy i dlaczego należy używać funktorów aplikacyjnych w Scali
Wiem, że Monad
można wyrazić w Scali następująco:
trait Monad[F[_]] {
def flatMap[A, B](f: A => F[B]): F[A] => F[B]
}
Widzę, dlaczego jest to przydatne. Na przykład, podane dwie funkcje:
getUserById(userId: Int): Option[User] = ...
getPhone(user: User): Option[Phone] = ...
Mogę łatwo napisać funkcję getPhoneByUserId(userId: Int)
ponieważ Option
jest monadą:
def getPhoneByUserId(userId: Int): Option[Phone] =
getUserById(userId).flatMap(user => getPhone(user))
...
Teraz widzę Applicative Functor
w Scali:
trait Applicative[F[_]] {
def apply[A, B](f: F[A => B]): F[A] => F[B]
}
Zastanawiam się, kiedy powinienem użyć zamiast monad . Myślę, że zarówno opcja, jak i lista są Applicatives
. Czy mógłbyś podać proste przykłady użycia apply
z opcją i listą oraz wyjaśnić dlaczego powinienem go używać zamiast flatMap
?
2 answers
Do cytuję siebie:
Więc po co w ogóle przejmować się funkcjami aplikacyjnymi, skoro mamy monady? Po pierwsze, po prostu nie jest możliwe dostarczenie instancji monad dla niektóre abstrakcje, z którymi chcemy pracować-
Validation
to doskonały przykład.Po drugie (i relatywnie), jest to po prostu solidna praktyka rozwojowa do wykorzystania najmocniejsza abstrakcja, która wykona zadanie. W zasada może to pozwolić na optymalizacje, które w przeciwnym razie nie be możliwe, ale co ważniejsze sprawia, że kod, który piszemy bardziej wielokrotnego użytku.
Aby rozwinąć nieco pierwszy akapit: czasami nie masz wyboru między kodem monadycznym a kodem aplikacyjnym. Zobacz resztę tej odpowiedzi dla dyskusji o tym, dlaczego warto użyć Scalaz Validation
(który nie ma i nie może mieć instancji monad) do modelowania
/ align = "left" /
Co do punktu optymalizacji: prawdopodobnie minie trochę czasu zanim to będzie ogólnie istotne w Scali lub Scalaz, ale patrz na przykład dokumentacja dla Haskella Data.Binary
:
Styl aplikacji może czasami skutkować szybszym kodem, ponieważ
binary
spróbuje zoptymalizować kod grupując odczyty razem.
Pisanie kodu aplikacyjnego pozwala uniknąć niepotrzebnych twierdzeń o zależnościach między obliczeniami-twierdzeń, do których zobowiąże Cię podobny kod monadyczny. Wystarczająco inteligentna Biblioteka lub kompilator może W zasadzie przyjmować zaletą tego faktu.
Aby ten pomysł był bardziej konkretny, rozważ następujący kod monadyczny:
case class Foo(s: Symbol, n: Int)
val maybeFoo = for {
s <- maybeComputeS(whatever)
n <- maybeComputeN(whatever)
} yield Foo(s, n)
for
-rozumienie desugarów do czegoś mniej więcej takiego jak:
val maybeFoo = maybeComputeS(whatever).flatMap(
s => maybeComputeN(whatever).map(n => Foo(s, n))
)
Wiemy, że maybeComputeN(whatever)
nie zależy od s
(zakładając, że są to dobrze zachowujące się metody, które nie zmieniają jakiegoś zmiennego stanu za kulisami), ale kompilator tego nie robi-z jego perspektywy musi wiedzieć s
, zanim zacznie obliczać n
.
Wersja aplikacyjna (przy użyciu Scalaz) wygląda tak:
val maybeFoo = (maybeComputeS(whatever) |@| maybeComputeN(whatever))(Foo(_, _))
Tutaj wyraźnie stwierdzamy, że nie ma zależności między tymi dwoma obliczeniami.
(i tak, ta |@|
składnia jest dość okropna-zobacz ten wpis na blogu dla niektórych dyskusji i alternatyw.)
Ostatni punkt jest jednak naprawdę najważniejszy. Wybór najmniejszego potężnego narzędzia, które rozwiąże twój problem, jest niezwykle potężną zasadą. Czasami naprawdę potrzebujesz kompozycji monadycznej-na przykład w metodzie getPhoneByUserId
- ale często tego nie robisz.]}
Szkoda, że zarówno Haskell, jak i Scala obecnie sprawiają, że praca z monadami jest o wiele wygodniejsza (składniowo itp.) niż praca Z FUNKCJAMI aplikacyjnymi, ale jest to głównie kwestia wypadku historycznego, a rozwój jak nawiasy idiomowe są krokiem w dobrym kierunku.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:04
Funktor służy do podnoszenia obliczeń do kategorii.
trait Functor[C[_]] {
def map[A, B](f : A => B): C[A] => C[B]
}
I działa doskonale dla funkcji jednej zmiennej.
val f = (x : Int) => x + 1
Ale dla funkcji 2 i więcej, po przeniesieniu do kategorii, mamy następujący podpis:
val g = (x: Int) => (y: Int) => x + y
Option(5) map g // Option[Int => Int]
I jest podpisem funktora aplikacyjnego. Aby zastosować następującą wartość do funkcji g
- potrzebny jest funktor aplikowalny.
trait Applicative[F[_]] {
def apply[A, B](f: F[A => B]): F[A] => F[B]
}
I wreszcie:
(Applicative[Option] apply (Functor[Option] map g)(Option(5)))(Option(10))
Funktor aplikacyjny jest funktorem stosującym wartość specjalna (wartość w kategorii) do funkcji Podnoszonej.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-01-17 12:33:00