Haskell: Я неправильно понимаю, как можно использовать стрелки? - PullRequest
17 голосов
/ 10 сентября 2010

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

{-# LANGUAGE Arrows#-}
module StatefulFunc where

import Control.Category
import Control.Arrow

newtype StatefulFunc a b = SF { unSF :: a -> (StatefulFunc a b, b) }

idSF :: StatefulFunc a a
idSF = SF $ \a -> (idSF, a)

dotSF :: StatefulFunc b c -> StatefulFunc a b -> StatefulFunc a c
dotSF f g = SF $ \a -> 
    let (g', b) = unSF g a
        (f', c) = unSF f b
    in (dotSF f' g', c)

instance Category StatefulFunc where
  id = idSF
  (.) = dotSF

arrSF :: (a -> b) -> StatefulFunc a b
arrSF f = ret
  where ret = SF fun
        fun a = (ret, f a)

bothSF :: StatefulFunc a b -> StatefulFunc a' b' -> StatefulFunc (a, a') (b, b')
bothSF f g = SF $ \(a,a') ->
    let (f', b) = unSF f a
        (g', b') = unSF g a'
    in (bothSF f' g', (b, b'))

splitSF :: StatefulFunc a b -> StatefulFunc a b' -> StatefulFunc a (b, b')
splitSF f g = SF $ \a ->
    let (f', b) = unSF f a
        (g', b') = unSF g a
    in (splitSF f' g', (b, b'))

instance Arrow StatefulFunc where
  arr  = arrSF
  first = flip bothSF idSF
  second = bothSF idSF
  (***) = bothSF
  (&&&) = splitSF

eitherSF :: StatefulFunc a b -> StatefulFunc a' b' -> StatefulFunc (Either a a') (Either b b')
eitherSF f g = SF $ \e -> case e of
      Left a -> let (f', b) = unSF f a in (eitherSF f' g, Left b)
      Right a' -> let (g', b') = unSF g a' in (eitherSF f g', Right b')

mergeSF :: StatefulFunc a b -> StatefulFunc a' b -> StatefulFunc (Either a a') b
mergeSF f g = SF $ \e -> case e of 
      Left a -> let (f', b) = unSF f a in (mergeSF f' g, b)
      Right a' -> let (g', b) = unSF g a' in (mergeSF f g', b)

instance ArrowChoice StatefulFunc where
  left = flip eitherSF idSF
  right = eitherSF idSF
  (+++) = eitherSF
  (|||) = mergeSF

Итак, после того, как я прошел через различные определения классов типов (не уверен, будет ли ArrowZero работать для этого, поэтому я пропустил его), я определил некоторые вспомогательные функции

evalSF :: (StatefulFunc a b) -> a -> b
evalSF f a = snd (unSF f a)

givenState :: s -> (s -> a -> (s, b)) -> StatefulFunc a b
givenState s f = SF $ \a -> let (s', b) = f s a in (givenState s' f, b)

И разработал пример использования

count :: StatefulFunc a Integer
count = givenState 1 $ \c _ -> (c+1, c)

countExample :: StatefulFunc a Integer
countExample = proc _ -> do
                  (count', one) <- count -< ()
                  (count'', two) <- count' -< ()
                  (count''', three) <- count'' -< ()
                  returnA -< three

Однако, когда я пытаюсь скомпилировать countExample, я получаю ошибки «Не в области» для count' и count'', что, как я полагаю, означает, что мне нужно вернуться к учебнику и прочитать, что может использоваться когда. Я думаю, что то, что мне действительно нравится, так или иначе, является чем-то вроде

countExample :: Integer
countExample =
  let (count', one) = unSF count ()
      (count'', two) = unSF count' ()
      (count''', three) = unSF count'' ()
  in three

Но это немного неловко, и я надеялся на что-то более естественное.

Может кто-нибудь объяснить, как я неправильно понимаю, как работают стрелки, и как их можно использовать? Есть ли в Arrows фундаментальная философия, по которой я скучаю?

1 Ответ

33 голосов
/ 10 сентября 2010

Может кто-нибудь объяснить, как я неправильно понимаю, как работают стрелки, и как их можно использовать? Есть ли в Arrows фундаментальная философия, по которой я скучаю?

У меня сложилось впечатление, что вы относитесь к этому Arrow, как если бы вы Monad. Я не знаю, считается ли это «фундаментальной философией», но между ними есть существенная разница, несмотря на то, как часто они совпадают. В некотором смысле ключевой вещью, которая определяет Monad, является функция join; как свернуть вложенную структуру в один слой. Они полезны из-за того, что позволяет join: вы можете создавать новые монадические слои в рекурсивной функции, изменять структуру Functor на основе ее содержимого и так далее. Но это не о Monad с, поэтому мы оставим это на этом.

Суть Arrow, с другой стороны, является обобщенная версия функции . Класс типа Category определяет обобщенные версии композиции функций и функции идентификации, в то время как класс типа Arrow определяет, как поднять обычную функцию до Arrow и как работать с Arrow s, которые принимают несколько аргументов ( в виде кортежей - Arrows не обязательно может быть карри!).

При базовом объединении Arrow s, как в вашей первой countExample функции, все, что вы действительно делаете, это что-то вроде сложной композиции функции . Посмотрите на свое определение (.) - вы берете две функции с состоянием и соединяете их в одну функцию с состоянием, при этом поведение изменения состояния обрабатывается автоматически.

Итак, главная проблема с вашим countExample в том, что он даже упоминает count' и тому подобное. Все это делается за кулисами, точно так же, как вам не нужно явно передавать параметр состояния при использовании нотации do в монаде State.

Теперь, поскольку нотация proc позволяет вам просто создавать большие составные Arrow с, чтобы на самом деле использовать вашу функцию с состоянием, вам нужно работать вне синтаксиса Arrow, как и вы нужно runState или что-то подобное для фактического запуска вычисления в монаде State. Ваш второй countExample такой же, но слишком специализированный. В общем случае ваша функция с состоянием отображает stream входов на stream выходов, что делает его конечным преобразователем состояния , так что runStatefulFunction, вероятно, будет возьмите ленивый список входных значений и преобразуйте их в ленивый список выходных значений, используя правый сгиб с unSF для подачи каждого к преобразователю по очереди.

Если вы хотите увидеть пример, пакет arrows включает Arrow трансформатор Automaton, который определяет что-то, почти идентичное вашему StatefulFunction, за исключением произвольного Arrow вместо простой функции, которую вы использовали.


О, и кратко рассмотрим взаимосвязь между Arrow с и Monad с:

Обычные Arrows - это только функции первого порядка. Как я уже говорил, их не всегда можно каррировать, а также они не всегда могут быть «применены» в том же смысле, в котором функция ($) применяет функции. Если вы действительно хотите более высокий порядок Arrows, класс типов ArrowApply определяет приложение Arrow. Это добавляет значительную мощность к Arrow и, среди прочего, позволяет использовать ту же функцию "свертывания вложенной структуры", которую обеспечивает Monad, что позволяет вообще определять экземпляр Monad для любого ArrowApply экземпляра.

В другом направлении, поскольку Monad s позволяют комбинировать функции, которые создают новую монадическую структуру, для любого Monad m можно говорить о «стрелке Клейсли», которая является функцией типа a -> m b. Стрелкам Клейсли для Monad можно дать экземпляр Arrow довольно очевидным способом.

За исключением ArrowApply и стрелок Клейсли, между классами типов нет особой интересной связи.

...