Haskell — помогите упростить функцию с расширенными функциями типов

Я пишу интерпретатор для динамически типизированного языка в Haskell.

Как и большинству интерпретаторов языков с динамической типизацией, моя программа также нуждается в проверке типов во время выполнения. Один из наиболее часто используемых кодов в моей программе:

interpreterFunction :: Value -> SomeMonadicContext ReturnType
interpreterFunction p = do
    VStr s <- ensureType p TString
    ..
    some code that uses s

Здесь я проверяю, что p имеет тип TString, и после этого я деструктурирую его с помощью VStr s <- .... Это никогда не дает сбоев, потому что VStr — это единственное значение, имеющее тип TString.

Мои структуры данных в основном таковы:

data Value = VStr String | VInt Int | VBool Bool
data Type  = TStr | TInt | TBool

Поэтому я разделяю свои значения в зависимости от их типов. т.е. У меня есть только один конструктор значений с типом TStr.

Теперь мне интересно, есть ли способ упростить мою функцию ensureType и деструктурирующий код. Например, возможно ли такое:

interpreterFunction p = do
    s <- ensureType p
    ..
    same code that uses s

Здесь из кода после s <- можно сделать вывод, что s имеет тип String, и статически известно, что только конструктор Value, который имеет часть String, является VStr, поэтому ensureType возвращает String после динамической проверки, является ли p VStr.

Я на самом деле понятия не имею, имеет ли это смысл или возможно. Я просто ищу способы улучшить свой дизайн с помощью расширенных функций системы типов Haskell.

Любая помощь будет оценена.


person sinan    schedule 24.02.2013    source источник
comment
Это может помочь описать эффект ensureType при наличии данных, отличных от TString.   -  person AndrewC    schedule 25.02.2013
comment
На данный момент кажется, что вы можете использовать сопоставление с образцом, чтобы переписать его как interpreterFunction (VStr s) = some code that uses s, за которым следует interpreterFunction _ = some error handling code. В противном случае, я думаю, вам следует внимательно изучить защиты шаблонов, потому что они могут оказывать уточняющее влияние на ваш код.   -  person AndrewC    schedule 25.02.2013
comment
То, что вы просите, имеет смысл, и похоже, что TypeFamilies — это то, что ищете (хотя и не обязательно единственное или лучшее решение). Они позволяют вам определять функции типов (например, от String до VStr, где VStr — новый тип), которые могут быть инъективными или нет.   -  person jberryman    schedule 25.02.2013
comment
@AndrewC, простое сопоставление с образцом у меня не работает из-за некоторых деталей, которые я не упомянул в вопросе. в принципе я и хочу от него throwError $ TypeError expected found и еще кое-что сделать.   -  person sinan    schedule 25.02.2013
comment
@jberryman может ли кто-нибудь дать мне несколько советов о том, как TypeFamilies могут помочь?   -  person sinan    schedule 25.02.2013


Ответы (2)


Да, на самом деле вы можете сделать это с помощью классов типов. Разумно это или нет — спорный вопрос (для вашего простого типа Value сопоставление с образцом, вероятно, является лучшим решением), но все равно интересно :)

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Main where

data Value = VStr String | VInt Int | VBool Bool

class FromValue a where
    fromValue :: Value -> Maybe a
instance FromValue String where
    fromValue (VStr s) = Just s
    fromValue _ = Nothing

ensureType :: (Monad m, FromValue a) => Value -> m a
ensureType = maybe (fail "type error!") return . fromValue

interpreterFunction :: Value -> IO ()
interpreterFunction val = 
    do s <- ensureType val
       putStrLn s

main =
    do interpreterFunction (VStr "asd")
       interpreterFunction (VInt 1)

Отпечатки:

asd
*** Exception: user error (type error!)

Вы также можете использовать расширение ScopedTypeVariables, чтобы указать определенный тип, когда его невозможно вывести:

{-# LANGUAGE ScopedTypeVariables #-}
interpreterFunction2 :: Value -> IO ()
interpreterFunction2 val = 
    do (s :: String) <- ensureType val
       print s

Кстати, ваш первоначальный подход кажется мне немного "неправильным":

VStr s <- ensureType p TString

Хотя вы можете быть уверены (самоанализом), что ensureType x TString никогда не возвращает ничего, кроме строки, это не обеспечивается системой типов, и соответствие шаблону не является исчерпывающим. Это не большая проблема, но вы можете легко исключить возможность сбоя во время выполнения этой функции, используя вместо этого специальную функцию "извлечение строки":

ensureString :: (Monad m) => Value -> m String
{- ... -}
s <- ensureString val
person Niklas B.    schedule 25.02.2013
comment
отлично, это решение полезно для меня, а также довольно просто. - person sinan; 25.02.2013

1. Чистый, понятный простой способ:

Я думаю, что тип союза с тегами, который вы определяете

data Value = VStr String | VInt Int | VBool Bool

имеет всю необходимую проверку типов во время выполнения, встроенную как простое старое сопоставление с образцом, и в его обертке в некоторых расширенных функциях системы типов отсутствует чистое и ясное решение:

interpreterFunction :: Value -> SomeMonadicContext ReturnType
interpreterFunction (Vstr s) = do
    some code that uses s
interpreterFunction _ = do
    some error handling code

Тебе понравилось:

interpreterFunction p = do
    s <- ensureType p
    ..
    same code that uses s

говоря

«Здесь из кода после s ‹- можно сделать вывод, что s имеет тип String, и статически известно, что единственным конструктором Value, который имеет часть String, является VStr, поэтому sureType возвращает String после динамической проверки, является ли p VStr.

Моя версия также динамически проверяет, является ли p VStr. Статически известно, что единственным конструктором Value, который имеет часть String, является VStr, но на самом деле это довольно сложно использовать.

2. Менее чистый способ, основанный на классе типов

Нам нужно создать экземпляр String, поэтому нам нужно

{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}

class FromValue a where 
   ensureType :: Value -> SomeMonadicContext a

Примеры:

instance FromValue String where
   ensureType (VStr s) = return s
   ensureType  _       = fail $ unlines 
                          ["Yikes, I'd rather have thrown a Nothing than a fail,"
                          ,"but then I'd have to have detagged the Just later"
                          ,"and then I may as well have used solution 1 anyway!"]

instance FromValue Int where
   ensureType (VInt i) = return i
   ensureType  _       = fail "Oh no, do I really have to use catch?"

Что дало бы

interpreterFunction :: Value -> IO String
interpreterFunction p = do
    s <- ensureType p
    return $ s ++ ", yay!" -- example String-specific code.

Недостаток этого заключается в том, что вместо выбора подходящего ответа пользователю происходит отказ. Вы можете использовать catch для восстановления, но опять же, вы проделываете очень много работы только для того, чтобы воспроизвести настраиваемые функции обработки регистра, которые предоставляет решение сопоставления с образцом.

2b. Может быть?

Использование типа данных Maybe в ensureType позволит избежать досадного сбоя/улова, но тогда вам придется выполнить Just s <- ensureType p или использовать функцию maybe, и все это по крайней мере столько же работы, сколько и обычное совпадение с шаблоном.

3. Что-то умное и новое, но неуместное

В качестве альтернативы, если вашей основной целью является использование классных и новых функций системы типов, вы можете использовать GADT для репликации экзистенциальных типов или Dynamic, чтобы сделать это более структурированным способом с динамической типизацией, но не делайте этого; ни один из них не обладает ясностью, простотой и элегантностью простого сопоставления с образцом.

person AndrewC    schedule 25.02.2013
comment
Большое спасибо за ваш ответ. возможно, мне следовало добавить пару слов о том, почему сопоставление с образцом здесь не решает мою проблему, но я просто хотел, чтобы вопрос был коротким. - person sinan; 25.02.2013
comment
@sinan Ах, возможно, было бы неплохо обновить страницу на некоторое время после того, как вы попросите ответить на комментарии под вашим вопросом, чтобы получить более индивидуальные ответы. У меня сейчас нет времени давать вам ответы на типичные семейства, но это интересно. Возможно, вы могли бы добавить РЕДАКТИРОВАТЬ: меня также интересует, есть ли решение для семейств типов в нижней части вашего вопроса на случай, если это вызовет интерес у кого-то еще. Поб хвил. - person AndrewC; 28.02.2013