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 ?

Author: Michael, 2013-11-09

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.

 76
Author: Travis Brown,
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.

 20
Author: Yuriy,
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