Что происходит, когда я сочиняю * с + в Haskell?
Я пытаюсь понять результат
(*) . (+)
в 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
Но это не скомпилируется. Я пытаюсь пройти логические шаги при их составлении, но я не совсем понимаю, как это получается с этим результатом (и каков результат).
Ответы
Ответ 1
Я понимаю, как вы себя чувствуете. Я обнаружил, что композицию функции довольно сложно понять сначала. То, что помогло мне разобраться, было подписями типа. Рассмотрим:
(*) :: 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.). g означает в Haskell?
Надеюсь, что это поможет.
Ответ 2
(*)
и (+)
оба имеют подпись типа 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
, которая является ТАКЖЕ числом.
Причина (*)
предполагает, что функция должна соответствовать типам, когда она ожидает два аргумента, но эта функция должна быть числом, потому что (*)
работает только с числами.
Действительно, эта функция не имеет никакого смысла.
Ответ 3
Некоторые расширения сначала:
{-# 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
.
ИЗМЕНИТЬ
Можно избежать использования расширения 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 Добавлены лучшие ссылки.
Ответ 4
Пусть:
m = (*)
a = (+)
затем
(m.a) x = (m (a x)) = m (a x)
Теперь m
ожидает a Num a
в качестве параметра, с другой стороны (a x)
, т.е. (x +)
является унарной функцией (a -> a)
по определению (+)
. Я предполагаю, что произошло то, что GHC пытается объединить эти два типа, так что если у вас есть тип, который является как числом, так и унарной функцией, m
может принимать число и унарную функцию и возвращать унарную функцию, так как они считаются одинаковыми.
Как указал @Syd, это объединение не имеет смысла для любых обычных типов чисел, таких как целые числа и числа с плавающей запятой.
Ответ 5
Здесь есть хорошие ответы, но позвольте мне быстро указать несколько шагов, по которым вы поступили не так.
Во-первых, правильное определение состава функции
(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)
не выполняется.