Haskell Напишите Monad для выражений

Я пытаюсь разработать встроенный язык, где операции могут поднимать определенные флаги в зависимости от значений. Я предвижу работу со скалярными значениями, а также с векторами (например, map, fold и т. Д.). Моя идея - использовать Writer Monad для отслеживания флагов. Упрощенный пример, где фактическим типом является "Int" и флаг поднимается, если любой из аргументов равен 0.

import Control.Monad.Identity
import Control.Monad.Writer
import Data.Monoid    

type WInt = Writer Any Int

bplus :: Int -> Int -> WInt
bplus a b =
    do
      tell (Any (a == 0 || b == 0)) ;
           return (a+b)

wbplus :: WInt -> WInt -> WInt
wbplus wa wb =
    do
      a <- wa ;
      b <- wb ;
      tell (Any (a == 0 || b == 0)) ;
           return (a+b)

ex0 = runWriter (bplus 1 2) 
ex1 = runWriter (bplus 0 2)

ex2 = runWriter (wbplus (return 1) (return 2))
ex3 = runWriter (wbplus (return 0) (return 2))

ex4 = runWriter (wbplus (wbplus (return 1) (return 2)) (return 2))
ex5 = runWriter (wbplus (wbplus (return 0) (return 2)) (return 2))
ex6 = runWriter (wbplus (wbplus (return 1) (return 2)) (return 0))

Я немного не уверен, каков наилучший способ реализовать это. Некоторые вопросы:

  1. Должен ли я определить все операции, как я сделал для bplus или как для wbplus, Позже, кажется, облегчает композицию. Но использовать foldM бинарный оператор должен иметь тип Int -> Int -> WInt,

  2. Какой будет подходящий тип для списков: Writer Any [Int] или же [Wint]?

Любые предложения или мысли приветствуются.

1 ответ

Решение

Вы можете получить bplus от wbplus и наоборот, используя соответствующие монадические операции:

import Control.Monad

apM2 :: Monad m => (a -> b -> m c) -> m a -> m b -> m c
apM2 f ma mb = do
  a <- ma
  b <- mb
  f a b

pureM2 :: Monad m => (m a -> m b -> m c) -> a -> b -> m c
pureM2 f a b = f (return a) (return b)

Они противоположны друг другу, что видно из типовых подписей их композиций:

ghci> :t pureM2 . apM2
pureM2 . apM2 :: Monad m => (a -> b -> m c) -> a -> b -> m c

ghci> :t apM2 . pureM2
apM2 . pureM2 :: Monad m => (m a -> m b -> m c) -> m a -> m b -> m c

Теперь вы можете определить wbplus = apM2 bplus или же bplus = pureM2 wbplus, Нет определенного ответа, какой из них лучше, используйте свой вкус и суждение. TemplateHaskell идет с wbplus подход и определяет все операции для работы со значениями в Q монада. См. Language.Haskell.TH.Lib.

относительно [m a] против m [a], вы можете идти только в одном направлении (через sequence :: Monad m => [m a] -> m [a]). Хотели бы вы когда-нибудь пойти в противоположном направлении? Вы заботитесь о том, чтобы отдельные значения имели свои собственные флаги или вы бы предпочли аннотировать вычисления в целом флагами?

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

  1. Если вы решите представлять каждое значение как Writer Any a и все операции работают с ним, вы можете начать с newtype:

    {-# LANGUAGE GeneralizedNewtypeDeriving #-}
    
    import Control.Monad.Writer
    
    newtype Value a = Value (Writer Any a)
      deriving (Functor, Applicative, Monad)
    

    Теперь вы можете определить экземпляры стандартных классов типов для ваших значений:

    instance (Num a, Eq a) => Num (Value a) where
      va + vb = do
        a <- va
        b <- vb
        (Value . tell . Any) (b == 0 || a == 0)
        return (a + b)
      (*) = liftM2 (*)
      abs = fmap abs
      signum = fmap signum
      negate = fmap negate
      fromInteger = return . fromInteger
    
      instance Monoid a => Monoid (Value a) where
        mempty = pure mempty
        mappend = liftM2 mappend
    

    Для EDSL это дает огромное преимущество: краткость и синтаксическая поддержка от компилятора. Теперь вы можете написать getValue (42 + 0) вместо wbplus (pure 42) (pure 0),

  2. Если вместо этого вы не думаете о флагах как о части своих значений и скорее рассматриваете их как внешний эффект, лучше использовать альтернативный подход. Но вместо того, чтобы написать что-то вроде Writer Any [Int], используйте соответствующие классы из mtl: MonadWriter Any m => m [Int], Таким образом, если позже вы обнаружите, что вам нужно использовать другие эффекты, вы можете легко добавить их к некоторым (но не ко всем) операциям. Например, вы можете поднять ошибку в случае деления на ноль:

      data DivisionByZero = DivisionByZero
    
      divZ :: (MonadError DivisionByZero m, Fractional a, Eq a) => a -> a -> m a
      divZ a b
        | b == 0 = throwError DivisionByZero
        | otherwise = pure (a / b)
    
      plusF :: (MonadWriter Any m, Num a, Eq a) => a -> a -> m a
      plusF a b = do
        tell (Any (b == 0 || a == 0))
        return (a + b)
    

    Теперь вы можете использовать plusF а также divZ вместе в одной монаде, хотя они имеют разные эффекты. Если позже вам понадобится интегрироваться с какой-либо внешней библиотекой, эта гибкость пригодится.

Я не слишком задумывался об этом, но, возможно, вы могли бы объединить эти подходы, используя что-то вроде newtype Value m a = Value { getValue :: m a }, Удачи в изучении дизайна пространства:)

Другие вопросы по тегам