thedeemon: (Default)
[personal profile] thedeemon
Раз все молчат, напишу я. На днях вышел Окамл 4.0, в котором появились GADT. Рассказать о них можно на классическом примере: AST. Пусть у нас есть язычок с целыми числами, которые можно складывать и сравнивать. Результат сложения - целое число, результат сравнения - булев. Есть выражение if, в котором должно быть булево условие и пара выражений одинакового типа. Раньше, если мы все выражения этого язычка описали бы одним типом, то булевы и численные выражения оказались бы взаимозаменяемы, и проверку типов пришлось бы встраивать в интерпретатор, делать ее в рантайме. Если разнести численные и булевы выражения по разным типам, то можно было бы статически гарантировать, что условие в if'e точно будет булевым выражением, но пришлось бы делать разные функции для интерпретации и других операций над AST - как минимум одну для булевых выражений и одну для численных. С GADT можно все описать одним типом и одной функцией, не потеряв статических гарантий:
type 'a expr =  
  | I   : int -> int expr 
  | Add : int expr * int expr -> int expr 
  | Gt  : int expr * int expr -> bool expr 
  | If  : bool expr * 'a expr * 'a expr -> 'a expr;; 
 
let rec eval : type a . a expr -> a = function 
  | I n -> n 
  | Add(e1, e2) -> eval e1 + eval e2 
  | Gt(e1, e2)  -> eval e1 > eval e2 
  | If(cond, thn, els) -> if eval cond then eval thn else eval els;; 

Тип expr устроен таким образом, что в условии у if'a может оказаться только булево выражение, а складываться и сравниваться могут только численные. При этом функция eval для целочисленных выражений возвращает int, а для булевых - bool:
 
let e1 = If( Gt( Add(I 5, I 2), I 6 ),  I 1, I 0) in 
let b = Gt(I 1, I 2) in 
let e2 = If(b, b, b) in 
print_int (eval e1); 
Printf.printf " %b" (eval e2);; 

Результат вычисления выражения if имеет тот же тип, что оба подвыражения-ветки, причем их тип должен быть одинаковым, иначе компилятор не даст построить такое if-выражение.

А какие полезные применения GADT'ам знаете вы?

Date: 2012-07-30 01:05 pm (UTC)
From: [identity profile] migmit.livejournal.com
> вот я как-то вбросил на тему того, что GADT'ы нужны только для "typed evaluator of dsl", до сих пор никто не опроверг.

Ну, это зависит от понимания, что есть "typed evaluator of dsl", потому что при желании сюда можно притянуть за уши любой пример.

Вот, допустим, пишем мы библиотеку для работы со списками, но списками не конкретных типов - это Data.List, - а произвольных типов, поддерживающий заданный интерфейс. Такая библиотека будет содержать тип-обёртку:
data Wrapper c where Wrapper :: c a => a -> Wrapper c
type HeteroList c = [Wrapper c]

Без GADT-ов будет тяжело.

Date: 2012-07-30 01:12 pm (UTC)
From: [identity profile] gds.livejournal.com
а почему бы не сделать тупо? Выделить этот интерфейс в запись (или можно в объект, или в модульб если про окамл говорить), и складировать эти интерфейсы в список. Понятно, что решение тупое, но интересны его недостатки по сравнению с GADT'ами.

Date: 2012-07-30 01:24 pm (UTC)
From: [identity profile] migmit.livejournal.com
> Выделить этот интерфейс в запись (или можно в объект, или в модульб если про окамл говорить), и складировать эти интерфейсы в список.

Придётся для каждого класса делать отдельный тип записей, по сути дублируя существующий код. Соблюсти принцип DRY вполне можно, но... при помощи GADT-ов:
data Record c a where Record :: c a => Record c a

Date: 2012-07-30 01:52 pm (UTC)
From: [identity profile] nponeccop.livejournal.com
{-# LANGUAGE ExistentialQuantification #-}

data Obj = forall a. (Show a) => Obj a

xs :: [Obj]
xs = [Obj 1, Obj "foo", Obj 'c']

doShow :: [Obj] -> String
doShow [] = ""
doShow ((Obj x):xs) = show x ++ doShow xs

main = putStrLn $ doShow xs

В Окамле экзистеншиалы на функторах вроде как есть http://caml.inria.fr/pub/ml-archives/caml-list/2004/01/be82a912316130893f08204cbd86e0b5.en.html
Edited Date: 2012-07-30 01:54 pm (UTC)

Date: 2012-07-30 04:48 pm (UTC)
From: [identity profile] gds.livejournal.com
да, так. Только не обязательно функторы, в простом случае даже тупых лямбд хватает. Классический трюк с упаковыванием "2 forall 1 exists" остаётся в силе, но и без этого existential types есть. Остальное -- удобства, не обязательные, но желательные. И каждый дрочит как ему приятнее.

Date: 2012-07-30 04:45 pm (UTC)
From: [identity profile] gds.livejournal.com
вот, я тоже в сторону http://thedeemon.livejournal.com/51630.html?thread=655790#t655790 смотрел. Ну, в окамле будет чуть более явно: http://paste.in.ua/4565/

Date: 2012-07-30 05:59 pm (UTC)
From: [identity profile] migmit.livejournal.com
Ага, значит, экзистеншиалы за часть гадтов не считаем? Ладно, как насчёт типа Record выше? Это не просто экзистеншиал.

Date: 2012-07-30 06:27 pm (UTC)
From: [identity profile] gds.livejournal.com
> Ага, значит, экзистеншиалы за часть гадтов не считаем?

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

> Ладно, как насчёт типа Record выше? Это не просто экзистеншиал.

а вот тут, честно признаюсь, я не копенгаген. Начиная с того, что не совсем понимаю (хотя и есть догадки), что именно "data Record c a" постановляет. Наверное, Вам будет лень "возиться со мной", поэтому можем проехать этот вопрос.

Date: 2012-07-30 06:47 pm (UTC)
From: [identity profile] migmit.livejournal.com
Давайте вы не будете говорить за меня, ладно?

Постановляет только одно: наличие, собственно говоря, этого самого instance c a. То есть, если у вас есть какая-то функция вроде, скажем
f :: C a => Int -> (a -> String) -> Char -- от балды, по большей части

то вы можете соорудить функцию
f' :: Record C a -> Int -> (a -> String) -> Char
f' Record n h = f n h

при этом паттерн-матчинг по первому аргументу в левой части обеспечит вам право использовать instance C a в правой. Вариант
f' _ n h = f n h

не прокатит.

Date: 2012-07-30 09:43 pm (UTC)
From: [identity profile] migmit.livejournal.com
Собственно, можно и попроще. Общеизвестно, что список — монада, а вот Set — не монада, так как там на каждый чих нужен instance Ord для того, что в этом Set находится. С GADT-ами получается легко и приятно:
data Set' a where Set' :: Ord a => Set a -> Set' a
instance Monad Set' where
  return = Set' . singleton
  Set' sa >>= h = Set' $ fold (\a sb -> case h a of Set' sb' -> sb `union` sb') empty sa

если я не напутал нигде.

Date: 2012-07-30 06:33 pm (UTC)
From: [identity profile] nponeccop.livejournal.com
> Это не просто экзистеншиал.

Это и не GADT. Он требует ConstraintKinds, т.е. большинство реализаций GADT этого не позволяют.

Date: 2012-07-30 06:34 pm (UTC)
From: [identity profile] migmit.livejournal.com
Wrapper тоже требует.

Date: 2012-07-30 06:39 pm (UTC)
From: [identity profile] nponeccop.livejournal.com
Ох, я не заметил. Оффтопик: а как значения Record конструировать? Параметра у конструктора нет, как у меня ниже:

data Record c a where Record :: c a => a -> Record c a

Date: 2012-07-30 06:48 pm (UTC)
From: [identity profile] migmit.livejournal.com
Не понял, в чём проблема с конструированием. Конструктор-то есть.

Date: 2012-07-30 07:12 pm (UTC)
From: [identity profile] nponeccop.livejournal.com
Он без параметров

Date: 2012-07-30 07:15 pm (UTC)
From: [identity profile] migmit.livejournal.com
Э-э-э... и? Мы конструкторов без параметров не видали, что ли? data Bool = False | True, например.

Date: 2012-07-30 07:25 pm (UTC)
From: [identity profile] nponeccop.livejournal.com
Так у Bool паттерн-матчинг позволяет извлекать полезную информацию. А здесь из полезной информации - голый словарь.

Как его использовать для соблюдения DRY в примере?

Date: 2012-07-30 07:41 pm (UTC)
From: [identity profile] migmit.livejournal.com
> А здесь из полезной информации - голый словарь.

Ничего себе "голый словарь". Это ж дофига информации!

> Как его использовать для соблюдения DRY в примере?

В примере — не слишком интересно. data Wrapper' c where Wrapper' :: Record c a -> a -> Wrapper' c, те же яйца, вид в профиль.

Куда интереснее использовать его для других целей. Например — как подсказать компилятору, что один класс есть подкласс другого? (Ну да, теоретически, компилятор и сам должен это выводить, но увы, не умеет)

А вот как:
data Void (c :: * -> Constraint) = Void -- это чисто для удобства
class c1 :<= c2 where
  isSubClass :: c1 a => Void c1 -> Record c2 a

Можно даже упростить создание инстансов, добавив в этот класс такое:
  default isSubClass :: c2 a => Void c1 -> Record c2 a
  isSubClass _ = Record

После этого, если есть какой-то
class Show a => Shit a where -- ну, там, какие-то методы

достаточно будет объявить
instance Shit :<= Show

и компилятор удовлетворится. А вот instance Show :<= Shit, или instance Num :<= Show не скомпилируются.

Зачем это нужно? Ну, например, можно будет писать такое:
instance (c :<= Show) => Show (Wrapper c) where
  show (Wrapper (x :: a)) = case isSubClass (Void :: Void c) :: Record Show a of Record -> show x

Без этого фокуса придётся отдельно делать
instance Show (Wrapper Show) where show (Wrapper x) = show x
instance Show (Wrapper Shit) where show (Wrapper x) = show x

и те де.
Edited Date: 2012-07-30 07:42 pm (UTC)

Date: 2012-07-30 01:30 pm (UTC)
From: [identity profile] nponeccop.livejournal.com
Ну гадты - это тяжелая артилерия, для этого экзистеншиалов хватило бы, которые можно сделать без гадтов

Profile

thedeemon: (Default)
Dmitry Popov

February 2026

S M T W T F S
12 34567
891011121314
15161718192021
22232425262728

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Mar. 8th, 2026 02:28 am
Powered by Dreamwidth Studios