Выбрать главу

:: Exp Int -> Exp Int -> Exp Int

Теперь у нашего типа Exp появился параметр, через который мы кодируем дополнительные ограничения

на типы операций. Теперь мы не сможем составить выражение Add ValTrue ValFalse, потому что оно не

пройдёт проверку типов.

Определим функцию evaclass="underline"

eval :: Exp a -> a

eval x = case x of

ValTrue

-> True

ValFalse

-> False

If p t e

-> if eval p then eval t else eval e

Val n

-> n

Add a b

-> eval a + eval b

Mul a b

-> eval a * eval b

Если eval получит логическое значение, то будет возвращено значение типа Bool, а на значение типа Exp

Int будет возвращено целое число. Давайте убедимся в этом:

Расширения | 255

*Prelude> :l Exp

[1 of 1] Compiling Exp

( Exp. hs, interpreted )

Ok, modules loaded: Exp.

*Exp> let notE x = If x ValFalse ValTrue

*Exp> let squareE x = Mul x x

*Exp>

*Exp> eval $ squareE $ If (notE ValTrue) (Val 1) (Val 2)

4

*Exp> eval $ notE ValTrue

False

*Exp> eval $ notE $ Add (Val 1) (Val 2)

< interactive>:1:14:

Couldn’t match expected type Bool’ against inferred type Int’

Expected type: Exp Bool

Actual type: Exp Int

In the return type of a call of Add’

In the second argument of ‘($)’, namely ‘Add (Val 1) (Val 2)’

Сначала мы определили две вспомогательные функции. Затем вычислили несколько значений. Haskell

очень часто применяется для построения компиляторов. Мы рассмотрели очень простой язык, но в более

сложном случае суть останется прежней. Дополнительный параметр позволяет нам закодировать в парамет-

ре тип функций нашего языка. Спрашивается: зачем нам дублировать вычисления в функции eval? Зачем нам

сначала кодировать выражение конструкторами, чтобы только потом получить то, что мы могли вычислить

и напрямую.

При таком подходе у нас есть полный контроль за деревом выражения, мы можем проводить дополни-

тельную оптимизацию выражений, если нам известны некоторые закономерности. Ещё функция eval может

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

Возможно этот язык гораздо мощнее Haskell по вычислительным способностям, но беднее в плане вырази-

тельности, гибкости синтаксиса. Тогда мы будем в функции eval проецировать разные конструкции Haskell

в конструкции другого языка. Такие программы называются предметно-ориентированными языками програм-

мирования (domain specific languages). Мы кодируем в типе Exp некоторую область и затем надстраиваем

над типом Exp разные полезные функции. На самом последнем этапе функция eval переводит всё дерево

выражения в значение или код другого языка.

Отметим, что не так давно было предложено другое решение этой задачи. Мы можем закодировать типы

функций в классе:

class E exp where

true

:: exp Bool

false

:: exp Bool

iff

:: exp Bool -> exp a -> exp a -> exp a

val

:: Int -> exp Int

add

:: exp Int -> exp Int -> exp Int

mul

:: exp Int -> exp Int -> exp Int

Преимуществом такого подхода является модульность. Мы можем спокойно разделить выражение на две

составляющие части:

class (Log exp, Arith exp) => E exp

class Log exp where

true

:: exp Bool

false

:: exp Bool

iff

:: exp Bool -> exp a -> exp a -> exp a

class Arith exp where

val

:: Int -> exp Int

add

:: exp Int -> exp Int -> exp Int

mul

:: exp Int -> exp Int -> exp Int

Интерпретация дерева выражения в этом подходе заключается в создании экземпляра класса. Например

создадим класс-вычислитель Eval:

newtype Eval a = Eval { runEval :: a }

instance Log Eval where

256 | Глава 17: Дополнительные возможности

true

= Eval True

false

= Eval False

iff p t e = if runEval p then t else e

instance Arith Eval where