Понимание функций как аппликативных в Haskell - PullRequest
0 голосов
/ 05 мая 2019

Недавно я пытался изучить Haskell с помощью «Learn You a Haskell» и действительно пытался понять функции в качестве Применительных.Я должен отметить, что при использовании других типов Аппликативов, таких как Списки и Возможно, я, кажется, достаточно хорошо понимаю, чтобы использовать их эффективно.

Как я склонен делать, пытаясь что-то понять, я пытался поиграть с таким количеством примеров.как я мог, и как только шаблон появляется, вещи имеют смысл.В качестве такового я попробовал несколько примеров.Прилагаются примечания к нескольким примерам, которые я попробовал вместе с диаграммой, которую я нарисовал, чтобы попытаться визуализировать происходящее.

enter image description here

Определение functне похоже на результат, но в моих тестах я использовал функцию со следующим определением:

funct :: (Num a) => a -> a -> a -> a

Внизу я попытался показать то же самое, что и на диаграммах, просто используя обычную математическую запись.

Так что все это хорошо, я могу понять шаблон, когда у меня есть какая-то функция с произвольным числом аргументов (хотя требуется 2 или более), и применить ее к функции, которая принимает один аргумент.Однако интуитивно этот шаблон не имеет для меня особого смысла.

Итак, у меня есть конкретные вопросы:

Что такое интуитивный способ понять шаблонЯ вижу, особенно если я рассматриваю Applicative как контейнер (как я вижу Maybe и списки)?

Каков шаблон, когда функция справа от <*> занимает большечем один аргумент (в основном я использовал функцию (+3) или (+5) справа)?

почему функция справа от <*> применяется ко второму аргументуфункции на левой стороне.Например, если функция с правой стороны была f(), то funct(a,b,c) превращается в funct (x, f(x), c)?

Почему она работает для funct <*> (+3), но не для funct <*> (+)?Более того, он работает на (\ a b -> 3) <*> (+)

. Любое объяснение, которое дает мне лучшее интуитивное понимание этой концепции, будет с благодарностью.Я читал другие объяснения, такие как в упомянутой книге, в которой функции объясняются в терминах ((->)r) или аналогичных шаблонов, но хотя я знаю, как использовать оператор ->) при определении функции, я не уверен, что понимаю ее вэтот контекст.

Дополнительные сведения:

Я также хочу включить фактический код, который я использовал, чтобы помочь мне сформировать диаграммы выше.

Сначала яопределил функцию, как я показал выше:

funct :: (Num a) => a -> a -> a -> a

На протяжении всего процесса я усовершенствовал функцию различными способами, чтобы понять, что происходит.

Далее я попробовал этот код:

funct a b c = 6 
functMod =  funct <*> (+3)
functMod 2 3

Неудивительно, что результат был 6

Так что теперь я попытался просто вернуть каждый аргумент прямо так:

funct a b c = a
functMod =  funct <*> (+3)
functMod 2 3 -- returns 2

funct a b c = b
functMod =  funct <*> (+3)
functMod 2 3 -- returns 5

funct a b c = c
functMod =  funct <*> (+3)
functMod 2 3 -- returns 3

Из этого я смог подтвердить, что вторая диаграмма - это то, что нужноместо.Я повторил эти паттерны, чтобы наблюдать и третью диаграмму (которая повторяет те же паттерны, которые повторяются сверху).

Ответы [ 3 ]

2 голосов
/ 05 мая 2019

Обычно вы можете понять, что функция делает в Haskell, если вы подставим его определение в несколько примеров. У вас уже есть некоторые примеры и определение, которое вам нужно, это <*> для (->) a, что это:

(f <*> g) x = f x (g x)

Я не знаю, найдете ли вы лучшую интуицию, чем просто использование определение несколько раз.

На вашем первом примере мы получаем это:

  (funct <*> (+3)) x
= funct x ((+3) x)
= funct x (x+3)

(Так как я ничего не мог сделать с funct <*> (+3) без далее параметр, который я только что применил к x - делайте это в любое время к.)

А остальные:

  (funct <*> (+3) <*> (+5)) x
= (funct x (x+3) <*> (+5)) x
= funct x (x+3) x ((+5) x)
= funct x (x+3) x (x+5)

  (funct <*> (+)) x
= funct x ((+) x)
= funct x (x+)

Обратите внимание, что вы не можете использовать один и тот же funct с обоими из них - в первый может принимать четыре числа, а второй - число и функция.

  ((\a b -> 3) <*> (+)) x
= (\a b -> 3) x (x+)
= (\b -> 3) (x+)
= 3

  (((\a b -> a + b) <*> (+)) x
= (\a b -> a + b) x (x+)
= x + (x+)
= type error
1 голос
/ 05 мая 2019

Как отметил Дэвид Флетчер , (<*>) для функций:

(g <*> f) x = g x (f x)

Есть две интуитивно понятные картинки (<*>) для функций, которые, хотя и не в состояниипредотвратите головокружение, может помочь сохранить равновесие при прохождении кода, который его использует.В следующих нескольких параграфах я буду использовать (+) <*> negate в качестве рабочего примера, поэтому вы можете попробовать его несколько раз в GHCi, прежде чем продолжить.

Первым изображением является (<*>) при применении результата функции к результату другой функции:

g <*> f = \x -> (g x) (f x)

Например, (+) <*> negate передаетаргумент для (+) и negate, выдавая функцию и число соответственно, а затем применяет одно к другому ...

(+) <*> negate = \x -> (x +) (negate x)

..., что объясняет, почему его результат всегда 0.

Второе изображение - (<*>) как вариация композиции функций, в которой аргумент также используется для определения того, какая вторая функция будет составлена:

g <*> f = \x -> (g x . f) x

Изс этой точки зрения (+) <*> negate отрицает аргумент и затем добавляет аргумент к результату:

(+) <*> negate = \x -> ((x +) . negate) x

Если у вас есть funct :: Num a => a -> a -> a -> a, funct <*> (+3) работает, потому что:

  • С точки зрения первого изображения: (+ 3) x - это число, и поэтому вы можете применить к нему funct x, заканчивая funct x ((+ 3) x), функцией, которая принимает два аргумента.

  • С точки зрения второго изображения: funct x - это функция (типа Num a => a -> a -> a), которая принимаети, таким образом, вы можете составить его с помощью (+ 3) :: Num a => a -> a.

С другой стороны, с помощью funct <*> (+) мы имеем:

  • С точки зрения первого изображения: (+) x - это не число, а функция Num a => a -> a, поэтому к нему нельзя применить funct x.

  • В терминахвторое изображение: тип результата (+), если рассматривать его как функцию одного аргумента ((+) :: Num a => a -> (a -> a)), равен Num a => a -> a (а не Num a => a), и поэтому вы не можете составить его с funct x(который ожидает Num a => a).

Для произвольного примера того, что работает с (+) в качестве второго аргумента (<*>), рассмотрим функцию iterate:

iterate :: (a -> a) -> a -> [a]

С учетом функции и начального значения, iterate генерирует бесконечный список путем многократного применения функции.Если мы перевернем аргументы на iterate, мы получим:

flip iterate :: a -> (a -> a) -> [a]

Учитывая, что проблема с funct <*> (+) заключалась в том, что funct x не будет принимать функцию Num a => a -> a, похоже, этоподходящий тип.И конечно же:

GHCi> take 10 $ (flip iterate <*> (+)) 1
[1,2,3,4,5,6,7,8,9,10]

(На тангенциальной ноте вы можете опустить flip, если используете (=<<) вместо (<*>). Это, однако, другая история)Чтобы использовать там интуитивно понятные картинки, вам придется учесть, что (<$>) для функций равно (.), что совсем немного мешает.Вместо этого проще просто рассмотреть все как поднятое приложение: в этом примере мы складываем результаты (^ 2) и (^ 3).Эквивалентное написание как ...

liftA2 (+) (^2) (^3)

... несколько подчеркивает это.Лично я чувствую, что одним из возможных недостатков написания liftA2 в этом параметре является то, что, если вы примените результирующую функцию прямо в том же выражении, вы получите что-то вроде ...

liftA2 (+) (^2) (^3) 5

... и видение liftA2, за которым следуют три аргумента, склоняет мой мозг к наклону.

1 голос
/ 05 мая 2019

Вы можете просматривать монаду функции как контейнер.Обратите внимание, что это действительно отдельная монада для каждого типа аргумента, поэтому мы можем выбрать простой пример: Bool.

type M a = Bool -> a

Это эквивалентно

data M' a = M' { resultForFalse :: a
               , resultForTrue :: a  }

и экземплярамможет быть определено

instance Functor M where            instance Functor M' where
  fmap f (M g) = M g'                 fmap f (M' gFalse gTrue) = M g'False g'True
   where g' False = f $ g False        where g'False = f $ gFalse
         g' True  = f $ g True               g'True  = f $ gTrue

и аналогично для Applicative и Monad.

Конечно, это исчерпывающее определение списка случаев станет совершенно непрактичным для типов аргументов с более чем несколькимивозможные значения, но это всегда один и тот же принцип.

Но важно отметить, что экземпляры всегда специфичны для одного конкретного аргумента .Итак, Bool -> Int и Bool -> String принадлежат одной и той же монаде, но Int -> Int и Char -> Int - нет.Int -> Double -> Int относится к той же монаде, что и Int -> Int, но только если вы рассматриваете Double -> Int как непрозрачный тип результата, который не имеет ничего общего с Int-> монадой.

Итак, если вы 'Если рассматривать что-то вроде a -> a -> a -> a, то это не вопрос аппликативов / монад, а Хаскеля в целом.И поэтому вы не должны ожидать, что изображение monad = container приведет вас куда угодно.Чтобы понять a -> a -> a -> a как члена монады, вам нужно выбрать, о какой из стрел вы говорите;в этом случае это только самый левый, т.е. у вас есть значение M (a->a->a) в монаде type M=(a->).Стрелки между a->a->a никоим образом не участвуют в монадическом действии;если они делают это в вашем коде, то это означает, что вы фактически смешиваете несколько монад.Прежде чем сделать это, вы должны понять, как работает одна монада, поэтому придерживайтесь примеров только с одной стрелкой функции.

...