haskell-notes

Приоритет указывается в строчках infixl 6 + и infixl 7 *. Цифра указывает на старшинство операции,

а суффикс l (от англ. left – левая) или r (от англ. right – правая) на ассоциативность.

Если мы создали свою функцию, мы можем определить для неё ассоциативность. Для этого мы пишем в

коде:

module Fixity where

import Prelude(Num(.. ))

infixl 4 ***

infixl 5 +++

infixr 5 ‘neg‘

(***) = (*)

(+++) = (+)

neg

= ()

Мы ввели новые операции и поменяли старшинство операций сложения и умножения местами и изме-

нили ассоциативность у вычитания. Проверим в интерпретаторе:

Prelude> :l Fixity

[1 of 1] Compiling Fixity

( Fixity. hs, interpreted )

Ok, modules loaded: Fixity.

*Fixity> 1 + 2 * 3

7

*Fixity> 1 +++ 2 *** 3

9

*Fixity> 1 2 3

4

*Fixity> 1 ‘neg‘ 2 ‘neg‘ 3

2

Посмотрим как это вычислялось:

1

+

2

*

3

==

1

+

(2

*

3)

1

+++

2

*** 3

==

(1

+++

2)

***

3

1

2

3

==

(1

2)

3

1 ‘neg‘ 2 ‘neg 3‘ ==

1 ‘neg‘ (2

‘neg‘ 3)

Также в Haskell есть директива infix это тоже самое, что и infixl.

Приоритет функции композиции

Посмотрим на приоритет функции композиции:

Prelude> :i (. )

(. ) :: (b -> c) -> (a -> b) -> a -> c

— Defined in GHC.Base

infixr 9 .

Она имеет высший приоритет. Она очень часто используется при определении функции в бесточечном

стиле. Такая функция похожа на конвейер функций:

fun a = fun1 a . fun2 (x1 + x2) . fun3 . (+x1)

Приоритет функции применения

Теперь посмотрим на полное определение функции применения:

infixr 0 $

($) :: (a -> b) -> a -> b

f $ x

=

f x

Ответ на вопрос о полезности этой функции кроется в её приоритете. Ей назначен самый низкий прио-

ритет. Она будет исполняться в последнюю очередь. Очень часто возникают ситуации вроде:

Приоритет инфиксных операций | 77

foldNat zero succ (Succ b) = succ (foldNat zero succ b)

С помощью функции применения мы можем переписать это определение так:

foldNat zero succ (Succ b) = succ $ foldNat zero succ b

Если бы мы написали без скобок:

… = succ foldNat zero succ b

То выражение было бы сгруппировано так:

… = (((succ foldNat) zero) succ) b

Но поскольку мы поставили барьер в виде операции ($) с низким приоритетом, группировка скобок

произойдёт так:

… = (succ $ ((foldNat zero) succ) b)

Это как раз то, что нам нужно. Преимущество этого подхода проявляется особенно ярко если у нас

несколько вложенных функций на конце выражения:

xs :: [Int]

xs = reverse $ map ((+1) . (*10)) $ filter even $ ns 40

ns :: Int -> [Int]

ns 0

= []

ns n

= n : ns (n 1)

В списке xs мы сначала создаём в функции ns убывающий список чисел, затем оставляем лишь чётные,

потом применяем два арифметических действия ко всем элементам списка, затем переворачиваем список.

Проверим работает ли это в интерпретаторе, заодно поупражняемся в композиционном стиле:

Prelude> let ns n = if (n == 0) then [] else n : ns (n 1)

Prelude> let even x = 0 == mod x 2

Prelude> let xs = reverse $ map ((+1) . (*10)) $ filter even $ ns 20

Prelude> xs

[21,41,61,81,101,121,141,161,181,201]

Если бы не функция применения нам пришлось бы написать это выражение так:

xs = reverse (map ((+1) . (*10)) (filter even (ns 40)))

5.3 Функциональный калькулятор

Мне бы хотелось сделать акцент на одном из вступительных предложений этой главы:

За счёт развитых средств составления новых функций в Haskell пользователь определяет лишь

базовые функции, получая остальные “на лету” применением двух-трёх операций, это выглядит

примерно как (2+3)*5, где вместо чисел стоят базовые функции, а операции + и * составляют

новые функции из простейших.

Такие обобщённые функции как id, const, (. ), map filter позволяют очень легко комбинировать различ-

ные функции. Бесточечный стиль записи функций превращает функции в простые значения или значения-

константы, которые можно подставлять в другие функции. В этом разделе мы немного потренируемся в пе-

регрузке численных значений и превратим числа в функции, функции и в самом деле станут константами.

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

том, что теперь мы применяем обычные операции сложения умножения к функциям, аргумент которых сов-

падает по типу. Например для того чтобы умножить функции t -> t+2 и t -> t+3 мы составляем новую

функцию t -> (t+2) * (t+3), которая получает на вход значение t применяет его к каждой из функций и

затем умножает результаты:

78 | Глава 5: Функции высшего порядка

module FunNat where

import Prelude(Show(.. ), Eq(.. ), Num(.. ), error)

instance Show (t -> a) where

show _ = error ”Sorry, no show. It’s just for Num”

instance Eq (t -> a) where

(==) _ _ = error ”Sorry, no Eq. It’s just for Num”

instance Num a => Num (t -> a) where

(+) = fun2 (+)

(*) = fun2 (*)

() = fun2 ()

abs

= fun1 abs

signum

= fun1 signum

fromInteger = const . fromInteger

fun1 :: (a -> b) -> ((t -> a) -> (t -> b))

fun1 = (. )

fun2 :: (a -> b -> c) -> ((t -> a) -> (t -> b) -> (t -> c))

fun2 op a b = t -> a t ‘op‘ b t

Функции fun1 и fun2 превращают функции, которые принимают значения, в функции, которые прини-

мают другие функции.

Из-за контекста класса Num нам пришлось объявить два фиктивных экземпляра для классов Show и Eq.

Загрузим модуль FunNat в интерпретатор и посмотрим что же у нас получилось:

Prelude> :l FunNat. hs

[1 of 1] Compiling FunNat

( FunNat. hs, interpreted )

Ok, modules loaded: FunNat.

*FunNat> 2 2

2

*FunNat> 2 5

2

*FunNat> (2 + (+1)) 0

3

*FunNat> ((+2) * (+3)) 1

12

На первый взгляд кажется что выражение 2 2 не должно пройти проверку типов, ведь мы применяем

значение к константе. Но на самом деле 2 это не константа, а значение 2 :: Num a => a и подспудно к двойке

применяется функция fromInteger. Поскольку в нашем модуле мы определили экземпляр Num для функций,

второе число 2 было конкретизировано по умолчанию до Integer, а первое число 2 было конкретизировано

до Integer -> Integer. Компилятор вывел из контекста, что под 2 мы понимаем функцию. Функция была

создана с помощью метода fromInteger. Эта функция принимает любое значение и возвращает двойку.

Далее мы складываем и перемножаем функции словно это обычные значения. Что интересно мы можем

составлять и такие выражения:

*FunNat> let f = ((+) (*))

*FunNat> f 1 2

1

Как была вычислена эта функция? Мы определили экземпляр функций для значений типа Num a => t

-> a. Если мы вспомним, что функция двух аргументов на самом деле является функцией одного аргумента:

Num a => t1 -> (t2 -> a), мы заметим, что тип Num a => (t2 -> a) принадлежит Num, теперь если мы

обозначим его за a’, то мы получим тип Num a’ => t1 -> a’, это совпадает с нашим исходным экземпляром.

Получается, что за счёт механизма частичного применения мы одним махом определили экземпляры Num

для функций любого числа аргументов, которые возвращают значение типа Num.

Итак функция f имеет вид:

t1 t2 -> (t1 + t2) (t1 * t2)

Подставим значения:

Функциональный калькулятор | 79

(t1 t2 -> (t1 + t2) (t1 * t2)) 1 2

(t2 -> (1 + t2) (1 * t2) 2

(1 + 2) (1 * 2)

3 2

1

Теперь давайте составим несколько выражений с обобщёнными функциями. Для этого добавим в модуль

FunNat директиву импорта функций из модуля Data.Function. Также добавим несколько основных функций

Читай продолжение на следующей странице
Добавить комментарии