Подтвердить что ты не робот

Что происходит, когда я сочиняю * с + в 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

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

4b9b3361

Ответ 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) не выполняется.