Как я могу улучшить свое понимание системы Haskell Type?

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

Вот пример. Я пытаюсь создать поток событий времени Пуассона:

import System.Random
import Numeric

bround :: (RealFloat r, Integral b) => b -> r -> r
bround places x = (fromIntegral (round ( x * exp))) / exp
       where exp = 10.0 ^ places

rndp = (bround 4)

myGen = (mkStdGen 1278267)

infinitePoissonStream :: (RandomGen g, Random r, RealFloat r) => r -> r -> g -> [r]
infinitePoissonStream rate start gen = next:(infinitePoissonStream rate next newGen)
        where  (rvalue, newGen) = random gen
               next = (start - log(rvalue) / rate)

printAll :: (RealFloat r) => [r] -> IO ()
printAll []     = return ()
printAll (x:xs) = do putStrLn (showFFloat (Just 8) x "")
                     printAll xs

main = do
       printAll (take 10 (infinitePoissonStream 1.0 0.0 myGen ) )

который меня подчёркивает:

mwe3.hs:23:8:
    No instance for (RealFloat r0) arising from a use of `printAll'
    The type variable `r0' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    Note: there are several potential instances:
      instance RealFloat Double -- Defined in `GHC.Float'
      instance RealFloat Float -- Defined in `GHC.Float'
      instance RealFloat Foreign.C.Types.CDouble
        -- Defined in `Foreign.C.Types'
      ...plus one other
    In a stmt of a 'do' block:
      printAll (take 10 (infinitePoissonStream 1.0 0.0 myGen))
    In the expression:
      do { printAll (take 10 (infinitePoissonStream 1.0 0.0 myGen)) }
    In an equation for `main':
        main
          = do { printAll (take 10 (infinitePoissonStream 1.0 0.0 myGen)) }

mwe3.hs:23:27:
    No instance for (Random r0)
      arising from a use of `infinitePoissonStream'
    The type variable `r0' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    Note: there are several potential instances:
      instance Random Bool -- Defined in `System.Random'
      instance Random Foreign.C.Types.CChar -- Defined in `System.Random'
      instance Random Foreign.C.Types.CDouble
        -- Defined in `System.Random'
      ...plus 33 others
    In the second argument of `take', namely
      `(infinitePoissonStream 1.0 0.0 myGen)'
    In the first argument of `printAll', namely
      `(take 10 (infinitePoissonStream 1.0 0.0 myGen))'
    In a stmt of a 'do' block:
      printAll (take 10 (infinitePoissonStream 1.0 0.0 myGen))

mwe3.hs:23:49:
    No instance for (Fractional r0) arising from the literal `1.0'
    The type variable `r0' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    Note: there are several potential instances:
      instance Fractional Double -- Defined in `GHC.Float'
      instance Fractional Float -- Defined in `GHC.Float'
      instance Integral a => Fractional (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      ...plus two others
    In the first argument of `infinitePoissonStream', namely `1.0'
    In the second argument of `take', namely
      `(infinitePoissonStream 1.0 0.0 myGen)'
    In the first argument of `printAll', namely
      `(take 10 (infinitePoissonStream 1.0 0.0 myGen))'

После того, как тыкаешься, я "исправил" его, изменив последнюю строку:

   printAll (take 10 (infinitePoissonStream 1.0 0.0 myGen ) :: [Double])

Теперь я хотел использовать арифметику с ограниченной точностью, поэтому я изменил следующую строку:

           next = rndp (start - log(rvalue) / rate)

и теперь он терпит неудачу:

mwe3.hs:15:29:
    Could not deduce (r ~ Double)
    from the context (RandomGen g, Random r, RealFloat r)
      bound by the type signature for
                 infinitePoissonStream :: (RandomGen g, Random r, RealFloat r) =>
                                          r -> r -> g -> [r]
      at mwe3.hs:12:26-83
      `r' is a rigid type variable bound by
          the type signature for
            infinitePoissonStream :: (RandomGen g, Random r, RealFloat r) =>
                                     r -> r -> g -> [r]
          at mwe3.hs:12:26
    In the first argument of `(-)', namely `start'
    In the first argument of `rndp', namely
      `(start - log (rvalue) / rate)'
    In the expression: rndp (start - log (rvalue) / rate)

Итак, я начинаю приходить к выводу, что я действительно не знаю, что делаю. Итак:

  • Может кто-нибудь объяснить, что мне здесь не хватает?
  • Любые указатели на главу и стих, где я мог бы рискнуть понять основные принципы?

Ответ 1

Проблема здесь в том, что GHC не может автоматически определить, какой RealFloat вы хотите использовать. Вы закодировали все в терминах RealFloat, а в main вы не предоставили конкретный тип для его использования, поэтому он останавливается и говорит "не мог понять". Вы можете исправить это, изменив хотя бы одну из ваших подписей типа, чтобы использовать Float или Double специально, но лучшим решением является просто указать, какой тип он должен находиться в main, например:

main = printAll $ take 10 (infinitePoissonStream 1.0 0.0 myGen :: [Double])

Когда вы добавляете [Double] в эту строку, вы явно указываете GHC, тип которого используется во время выполнения. Без этого он знает, как использовать RealFloat r и Random r, и выбрать несколько типов, а именно Float и Double. Либо будет работать для этого случая, но компилятор этого не знает.

Кроме того, я бы предложил несколько стилистических изменений, чтобы избавиться от некоторых из этих парнов:

import System.Random
import Numeric

bround :: (RealFloat r, Integral b) => b -> r -> r
bround places x = fromIntegral (round $ x * e) / e
       where e = 10.0 ^ places
       -- exp is a pre-defined function, shouldn't name a variable with it

-- Even if it trivial, you should add type signatures, it really helps others read your code faster
rndp = bround 4
myGen = mkStdGen 1278267

-- function application before operator application means you can remove some parens
infinitePoissonStream :: (RandomGen g, Random r, RealFloat r) => r -> r -> g -> [r]
infinitePoissonStream rate start gen = next : infinitePoissonStream rate next newGen
        where  (rvalue, newGen) = random gen
               next = start - log rvalue / rate

-- Start a new line at the beginning of a do block, the indentations are nicer
printAll :: (RealFloat r) => [r] -> IO ()
printAll []     = return ()
printAll (x:xs) = do
    putStrLn $ showFFloat (Just 8) x ""
    printAll xs

-- No need for a do block with only one statement
main = printAll $ take 10 (infinitePoissonStream 1.0 0.0 myGen :: [Double])

Эти изменения происходят главным образом из hlint.

Ответ 2

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

  • Если есть подпись типа в рассматриваемой функции, удалите ее и посмотрите, что-нибудь изменится. Если он компилируется, спросите ghci, что это за тип (используя :t). Если он не компилируется, по крайней мере сообщение об ошибке может быть достаточно разным, чтобы дать вам еще один ключ.
  • Если нет сигнатуры типа, добавьте ее. Даже если он не компилируется, сообщение об ошибке может дать вам еще один ключ.
  • Если это не помогает, временно добавьте объявления типов в каждое из выражений функции. (Часто вам нужно разбить некоторые выражения, чтобы увидеть, что действительно происходит. Возможно, вам также потребуется временно включить прагму ScopedTypeVariables.) Скомпилируйте снова и проверьте сообщения об ошибках.

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

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

printAll :: (RealFloat r) => [r] -> IO ()
printAll []     = return ()
printAll (x:xs) = do
  let temp1=showFFloat (Just 8) x "" :: String
  putStrLn temp1 :: IO ()
  printAll xs :: IO ()

main = do
  let temp2 = take 10 (infinitePoissonStream 1.0 0.0 myGen ) :: (RealFloat r) => [r]
  -- but if you make this change, it compiles:
  -- let temp2 = take 10 (infinitePoissonStream 1.0 0.0 myGen ) :: [Double]
  printAll temp2

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