Что происходит, когда я сочиняю * С + в Haskell?


Я пытаюсь понять результат

(*) . (+) 

В Хаскелле. Я знаю, что оператор композиции - это просто стандартная композиция математических функций-так что

(f . g) = f (g x)

Но:

(*) . (+) :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a

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

((*) . (+)) 1 2 :: Num a => a -> a
= (* (+ 1 2))

Что означает ( * ). (+) тип подписи? Я попробовал поиграть с ним чем-то вроде (просто совпадая с его подписью):

((*) . (+)) 1 (x -> x + 1) 1

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

5 45

5 ответов:

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

(*) :: Num x => x -> x -> x
(+) :: Num y => y -> y -> y
(.) :: (b -> c) -> (a -> b) -> a -> c
Теперь, когда вы пишете (*) . (+), это фактически то же самое, что (.) (*) (+) (т. е. (*) является первым аргументом для (.) и (+) является вторым аргументом для (.)):
(.) :: (b -> c) -> (a -> b) -> a -> c
       |______|    |______|
           |           |
          (*)         (+)

Следовательно, сигнатура типа (*) (то есть Num x => x -> x -> x) объединяется с b -> c:

(*) :: Num x => x -> x -> x -- remember that `x ->  x -> x`
                |    |____| -- is implicitly `x -> (x -> x)`
                |       |
                b ->    c

(.) (*) ::          (a -> b) -> a ->    c
                          |             |
                          |          |‾‾‾‾|
           Num x =>       x          x -> x

(.) (*) :: Num x => (a -> x) -> a -> x -> x

Отсюда сигнатура типа (+) (т. е. Num y => y -> y -> y) объединяется с Num x => a -> x:

(+) :: Num y => y -> y -> y -- remember that `y ->  y -> y`
                |    |____| -- is implicitly `y -> (y -> y)`
                |       |
       Num x => a ->    x

(.) (*) (+) ::  Num x                => a ->     x    ->    x
                                        |        |          |
                                        |     |‾‾‾‾|     |‾‾‾‾|
                              Num y  => y     y -> y     y -> y

(.) (*) (+) :: (Num (y -> y), Num y) => y -> (y -> y) -> y -> y
Я надеюсь, что это проясняет, откуда берутся Num (y -> y) и Num y. Вы остаетесь с очень странной функцией типа (Num (y -> y), Num y) => y -> (y -> y) -> y -> y.

Что делает его настолько странным, так это то, что он ожидает, что и y, и y -> y будут экземплярами Num. Понятно, что y должен быть примером Num, но как y -> y? Создание y -> y экземпляра Num кажется нелогичным. Это не может быть правдой.

Однако это имеет смысл, когда вы смотрите на то, какая функция композиция действительно делает:

( f  .  g ) = \z ->  f  ( g  z)

((*) . (+)) = \z -> (*) ((+) z)

Итак, у вас есть функция \z -> (*) ((+) z). Следовательно, z явно должен быть экземпляром Num, потому что к нему применяется (+). Таким образом, тип \z -> (*) ((+) z) - это Num t => t -> ..., где ... - это тип (*) ((+) z), который мы сейчас выясним.

Поэтому ((+) z) относится к типу Num t => t -> t, потому что для него требуется еще одно число. Однако, прежде чем он будет применен к другому числу, к нему применяется (*).

Следовательно, (*) ожидает, что ((+) z) является экземпляром Num, именно поэтому t -> t должен быть примером Num. Таким образом, ... заменяется (t -> t) -> t -> t и добавляется ограничение Num (t -> t), что приводит к типу (Num (t -> t), Num t) => t -> (t -> t) -> t -> t.

Способ, которым вы действительно хотите объединить (*) и (+), заключается в использовании (.:):

(.:) :: (c -> d) -> (a -> b -> c) -> a -> b -> d
f .: g = \x y -> f (g x y)

Следовательно, (*) .: (+) то же самое, что \x y -> (*) ((+) x y). Теперь два аргумента даются (+), гарантируя, что ((+) x y) действительно просто Num t => t, а не Num t => t -> t.

Следовательно, ((*) .: (+)) 2 3 5 есть (*) ((+) 2 3) 5 то, что есть (*) 5 5 то, что есть 25, что я считаю тем, что вы хотеть.

Обратите внимание, что f .: g также может быть записано как (f .) . g, а (.:) также может быть определено как (.:) = (.) . (.). Подробнее об этом можно прочитать здесь:

Что делает (f .). в смысле, в Хаскелле?

Надеюсь, это поможет.

(*) и (+) оба имеют сигнатуру типа Num a => a -> a -> a Теперь, если вы их сочините, вы получите что-то обалденное.

(*) . (+) :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a

Это потому, что (*) и (+) ожидают два "аргумента".

(+) с одним аргументом получает функцию. Оператор . ожидает эту функцию (a -> a, которую вы видите).

Вот значение (*) . (+)

                                       x     f          y
 (*) . (+) :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a

(*) . (+) отображение x f y в ((x +) * f) y, где f - функция от a до a, которая также является числом. Причина (*) функция ожидает, что типы будут совпадать, пока она ожидает два аргумента, но эта функция должна быть числом, потому что (*) работает только с числами.

На самом деле, эта функция вообще не имеет смысла.

Сначала некоторые расширения:

{-# LANGUAGE FlexibleContexts, FlexibleInstances, TypeSynonymInstances #-}

Как показывают другие ответы, ваша функция -

weird :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a
weird x g = (x +) * g
Но эта функция имеет не странную семантику.

Существует понятие списков различий. Соответственно, существует понятие разностных целых чисел. Я видел, что они используются только в зависимости от типизации (например, здесь , но это не единственный случай). Соответствующая часть определения является

instance Enum DiffInt where
    toEnum   n = (n +)
    fromEnum n = n 0

instance Num DiffInt where
    n + m = n . m
    n * m = foldr (+) id $ replicate (fromEnum n) m

В этом нет особого смысла. Haskell, но может быть полезен с зависимыми типами.

Теперь мы можем написать

test :: DiffInt
test = toEnum 3 * toEnum 4

Или

test :: DiffInt
test = weird 3 (toEnum 4)

В обоих случаях fromEnum test == 12.

EDIT

Можно избежать использования расширения TypeSynonymInstances:

{-# LANGUAGE FlexibleContexts, FlexibleInstances #-}

weird :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a
weird x g = (x +) * g

instance (Enum a, Num a) => Enum (a -> a) where
    toEnum   n = (toEnum n +)
    fromEnum n = fromEnum $ n (toEnum 0)

instance (Enum a, Num a) => Num (a -> a) where
    n + m = n . m
    n * m = foldr (+) id $ replicate (fromEnum n) m

type DiffInt = Int -> Int

Как и прежде, мы можем написать

test' :: DiffInt
test' = weird 3 (toEnum 4)

Но теперь мы можем также написать

-- difference ints over difference ints
type DiffDiffInt = DiffInt -> DiffInt

test'' :: DiffDiffInt
test'' = weird (toEnum 3) (toEnum (toEnum 4))

И

main = print $ fromEnum $ fromEnum test'

Отпечатки 12.

EDIT2 добавлены лучшие ссылки.

Пусть:

m = (*)
a = (+)

Затем

(m.a) x = (m (a x)) = m (a x) 

Теперь m ожидает Num a в качестве параметра, с другой стороны (a x), т. е. (x +) является унарной функцией (a -> a) по определению (+). Я предполагаю, что произошло то, что GHC пытается объединить эти два типа так, чтобы, если у вас есть тип, который является одновременно числом и унарной функцией, m может взять число и унарную функцию и вернуть унарную функцию, так как они считаются одним и тем же типом.

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

Здесь есть хорошие ответы, но позвольте мне быстро указать на несколько шагов, где вы ошиблись.

Во-первых, правильное определение состава функций -

(f . g) x = f (g x)

Вы пропустили x на LHS. Далее, вы должны помнить, что в Haskell h x y это то же самое, что и (h x) y. Итак, вопреки тому, что вы ожидали,

((*) . (+)) 1 2 = (((*) . (+)) 1) 2 = ((*) ((+) 1)) 2 = ((+) 1) * 2,
И теперь вы видите, почему это не удается. Кроме того,
((*) . (+)) 1 (\x -> x + 1) 1

Не работает, потому что ограничение Num (Int -> Int) не выполняется.