Как «concatMap» из mono-traversable способен «вытянуть» общий аргумент?

9

Я изучаю Haskell и делал простую программу DB-seed для Yesod, когда наткнулся на это поведение, которое мне трудно понять:

testFn :: Int -> Bool -> [Int]
testFn a b = if b then replicate 10 a else []

Сессия Йесод GHCI:

$ :t concatMap testFn [3]
concatMap testFn [3] :: Bool -> [Int]
$ (concatMap testFn [1,2,3]) True
[1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3]

Каким-то образом ему удалось «вытащить» этот второй «Bool» из каждого отображения в один аргумент с каррированием.

Стандартный базовый сеанс Prelude GHCI отказывается даже компилировать это выражение:

$ :t concatMap testFn [3]
error:
     Couldn't match type 'Bool -> [Int]' with '[b]'
      Expected type: Int -> [b]
        Actual type: Int -> Bool -> [Int]
     Probable cause: 'testFn' is applied to too few arguments
      In the first argument of 'concatMap', namely 'testFn'
      In the expression: concatMap testFn [3]

Оказывается, Йесод использует моно-проходимую библиотеку, которая имеет свою собственную concatMap:

$ :t concatMap
concatMap
  :: (MonoFoldable mono, Monoid m) =>
     (Element mono -> m) -> mono -> m

На моем нынешнем уровне понимания Haskell я не мог понять, как типы распределяются здесь. Может ли кто-нибудь объяснить мне (насколько это возможно для начинающих), как это делается? Какая часть testFnвыше соответствует Element monoтипу?

dimsuz
источник

Ответы:

6

Мы начнем с перечисления некоторых известных нам типов. (Мы притворяемся, что цифры Intдля простоты - это на самом деле не актуально.)

testFn :: Int -> Bool -> [Int]
[1,2,3] :: [Int]
True :: Bool

(concatMap testFn [1,2,3]) Trueтакой же, как concatMap testFn [1,2,3] True, поэтому concatMapдолжен иметь тип, соответствующий всем этим аргументам:

concatMap :: (Int -> Bool -> [Int]) -> [Int] -> Bool -> ???

где ???тип результата. Обратите внимание, что из-за правил ->ассоциативности связывается справа, поэтому приведенная выше типография такая же, как:

concatMap :: (Int -> (Bool -> [Int])) -> [Int] -> (Bool -> ???)

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

concatMap :: (MonoFoldable mono, Monoid m) =>
             (Element mono -> m              ) -> mono  -> m
concatMap :: (Int          -> (Bool -> [Int])) -> [Int] -> (Bool -> ???)

Ах-ха! У нас есть совпадение, если мы выберем mкак Bool -> [Int]и monoкак [Int]. Если мы делаем это, мы удовлетворяем ограничениям MonoFoldable mono, Monoid m(см. Ниже), и мы также имеемElement mono ~ Int , поэтому все проверяет тип.

Мы делаем вывод, что ???это [Int]из определенияm .

Об ограничениях: ведь MonoFoldable [Int]мало что можно сказать. [Int]явно список подобного типа с Intтипом элемента, и этого достаточно , чтобы сделать это в MonaFoldableс Intкак его Element.

Потому Monoid (Bool -> [Int])что это немного сложнее. У нас есть, что любой тип функции A -> Bявляется моноидом, если Bявляется моноидом. Это следует, выполняя операцию поточечно. В нашем конкретном случае мы полагаемся на [Int]моноид и получаем:

mempty :: Bool -> [Int]
mempty = \_ -> []

(<>) :: (Bool -> [Int]) -> (Bool -> [Int]) -> (Bool -> [Int])
f <> g = \b -> f b ++ g b
чи
источник
1
Отличное объяснение! То, как вы объясняли и выравнивали типы, это именно то, что я хотел увидеть, спасибо!
Димсуз