Непонимание ArrowLoop при использовании с Netwire

Следуя отличному ответу в этом сообщении, я пытаюсь получите рабочий пример ArrowLoop, в котором не используются стрелки. Мне неудобно использовать обозначения стрелок, пока я полностью не пойму, как стрелки работают под капотом. При этом я создал небольшую программу, которая, основываясь на моем (ограниченном) понимании Arrows, должна работать. Однако в конечном итоге он завершается ужасным <<loop>> исключением:

module Main where

import Control.Wire
import FRP.Netwire

farr :: SimpleWire (Int, Float) (String, Float)
farr = let
  fn :: Int -> Float -> ((String, Float), SimpleWire (Int, Float) (String, Float))
  fn i f = (("f+i: " ++ (show (fromIntegral i + f)), f + 0.1), loopFn)

  loopFn :: SimpleWire (Int, Float) (String, Float)
  loopFn = mkSFN $ \(i, f) -> fn i f
  in
   mkSFN $ \(i, _) -> fn i 0.0

main :: IO ()
main = do
  let sess = clockSession_ :: Session IO (Timed NominalDiffTime ())
  (ts, sess2) <- stepSession sess

  let wire = loop farr
      (Right s, wire2) = runIdentity $ stepWire wire ts (Right 0)

  putStrLn ("s: " ++ s)

  (ts2, _) <- stepSession sess2
  let (Right s2, _) = runIdentity $ stepWire wire2 ts (Right 1)

  putStrLn ("s2: " ++ s2)

Моя интуиция подсказывает мне, что <<loop>> исключение обычно возникает, когда вы не передаете начальное значение циклу. Разве я не сделал этого со строкой, содержащей fn i 0.0? Вывод не согласуется:

$ ./test
s: f+i: 0.0
test.exe: <<loop>>

Кто-нибудь знает, что я делаю не так?


person Mokosha    schedule 05.08.2014    source источник
comment
‹‹Loop›› Является ли GHC способом сообщить вам, что он обнаружил цикл оценки; Выражение, в котором во время оценки GHC оно возвращалось к тому же преобразователю.   -  person Boyd Stephen Smith Jr.    schedule 05.08.2014
comment
Хорошо, я бы хотел еще немного объяснить, почему это происходит в данном случае, поскольку я этого не ожидаю.   -  person Mokosha    schedule 05.08.2014


Ответы (1)


Основной причиной путаницы, казалось, были неразрывные отношения между ArrowLoop и mfix. Для непосвященных fix - это функция, которая находит фиксированную точку заданной функции:

fix :: (a -> a) -> a
fix f = let x = f x in x

mfix - это монадическое расширение этой функции, сигнатура типа которой, что неудивительно, такова:

mfix :: (a -> m a) -> m a

Так при чем здесь ArrowLoop? Итак, экземпляр ArrowLoop для Netwire запускает mfix второй аргумент переданного провода. Другими словами, рассмотрим сигнатуру типа для loop:

loop :: a (b, d) (c, d) -> a b c

В Netwire экземпляр ArrowLoop:

instance MonadFix m => ArrowLoop (Wire s e m)

Это означает, что тип функции loop при использовании с проводами:

loop :: MonadFix m => Wire s e m (b, d) (c, d) -> Wire s e m b c

Поскольку loop не принимает начальный аргумент типа d, это означает, что нет никакого способа инициализировать какой-либо обычный "цикл" по сети. Единственный способ получить из него значение - продолжать применять вывод в качестве ввода до тех пор, пока не будет найдено условие завершения, что аналогично тому, как работает fix. Провод, который передается в качестве аргумента loop, на самом деле никогда не проходит более одного раза, поскольку stepWire применяется к одному и тому же проводу снова и снова с разными входами. Только когда провод фактически производит фиксированное значение, функция выполняет шаг и создает еще один провод (который ведет себя так же, как и первый).

Для полноты, вот код моей первоначальной интуиции относительно того, как должен работать loop, который я назвал semiLoop:

semiLoop :: (Monad m, Monoid s, Monoid e) => c -> Wire s e m (a, c) (b, c) -> Wire s e m a b
semiLoop initialValue loopWire = let
  runLoop :: (Monad m, Monoid s, Monoid e) =>
             Wire s e m (a, c) (b, c) -> s -> a -> c -> m (Either e b, Wire s e m a b)
  runLoop wire ts ipt x = do
    (result, nextWire) <- stepWire wire ts (Right (ipt, x))
    case result of
      Left i -> return (Left i, mkEmpty)
      Right (value, nextX) ->
        return (Right value, mkGen $ \ts' ipt' -> runLoop nextWire ts' ipt' nextX)
  in
   mkGen $ \ts input -> runLoop loopWire ts input initialValue

Редактировать

После замечательного ответа, предоставленного Петром, комбинатор delay необходим для предотвращения loop комбинатор от расходящихся. delay просто создает буфер с одним значением между ленивым использованием следующего значения в mfix части цикла, описанного выше. Таким образом, идентичное определение semiLoop, приведенное выше:

semiLoop :: (MonadFix m, Monoid s, Monoid e) =>
            c -> Wire s e m (a, c) (b, c) -> Wire s e m a b
semiLoop initialValue loopWire = loop $ second (delay initialValue) >>> loopWire
person Mokosha    schedule 12.08.2014