Я пытаюсь реализовать динамически типизированный язык программирования на Haskell, который поддерживает три типа данных, назовем их A, B и C, и только для иллюстрации я позволю A = Integer, B = [Integer] и C = (Integer, Integer) (но вы можете игнорировать семантику этих типов, меня волнует не это).

Для того чтобы значения любого типа можно было взаимозаменяемо использовать в арифметических выражениях, я реализовал алгебраический тип данных Value:

data Value = A A
           | B B
           | C C

И поскольку я хочу иметь возможность складывать и умножать значения, я реализовал типовой класс OP:

class Op a where
  add :: a -> a -> a
  mul :: a -> a -> a

Сейчас я также хочу, чтобы мои типы неявно конвертировались друг в друга (когда два разных типа появляются в арифметическом выражении), согласно следующим правилам:

  • Если оба типа A, преобразование не происходит
  • Если один из типов является A, другой преобразуется в A
  • В противном случае оба типа преобразуются в B
  • .

Чтобы сделать это возможным, я реализовал еще один класс типов, ImplicitlyConvertible:

class ImplicitlyConvertible a where
  toA :: a -> A
  toB :: a -> B

Полный пример будет выглядеть следующим образом:

{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}

module Value where

type A = Integer

type B = [Integer]

type C = (Integer,Integer)

data Value = A A
           | B B
           | C C

class ImplicitlyConvertible a where
  toA :: a -> A
  toB :: a -> B

instance ImplicitlyConvertible A where
  toA = id
  toB = error "can't convert A to B"

instance ImplicitlyConvertible B where
  toA = sum
  toB = id

instance ImplicitlyConvertible C where
  toA   = sum
  toB c = [fst c, snd c]

instance ImplicitlyConvertible Value where
  toA v = case v of
    A a -> toA a
    B b -> toA b
    C c -> toA c
  toB v = case v of
    A a -> toB a
    B b -> toB b
    C c -> toB c

class Op a where
  add :: a -> a -> a
  mul :: a -> a -> a

instance Op A where
  add = (+)
  mul = (*)

instance Op B where
  add = zipWith (+)
  mul = zipWith (*)

valueOp :: (Value -> Value -> Value) -> (Value -> Value -> Value)
valueOp op (A v) v' = op (A v) (A $ toA v')
valueOp op v (A v') = op (A $ toA v) (A v')
valueOp op v v'     = op (B $ toB v) (B $ toB v')

instance Op Value where
  add = valueOp add
  mul = valueOp mul

У меня есть три проблемы с этим:

  • Тот факт, что toB фактически не реализован для A, кажется нечистым. Даже если он никогда не должен вызываться, я бы хотел избежать необходимости реализовывать его вообще.

  • instance ImplicitlyConvertible Value - это просто куча шаблонного кода, от которого я хотел бы избавиться.

  • Я не уверен, что моя реализация instance Op Value является разумной.

Может быть, я неправильно подхожу к этому вопросу? Как я могу реализовать все это более чисто?

Peter

Ответов: 1

Ответы (1)

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

  • Я не уверен, что моя реализация instance Op Value разумна.

Нет, ваша реализация instance Op Value не является разумной. Пробовали ли вы оценить ее на примере входных данных? Возможно, вы заметили, что она никогда не выдает результата. Проблема связана с тем, с чем вы вызываете valueOp. Кажется, что вы вызываете valueOp с полиморфной функцией add (или mult), но на самом деле это не так. Поскольку valueOp всегда принимает в качестве первого аргумента функцию на Values, определение add в вашем экземпляре Op Value всегда будет вызывать valueOp с функцией add, которая определена самим экземпляром. Это создает бесконечную рекурсию.

Как передать полиморфную функцию add в valueOp вместо этого? Рассмотрим такой тип:

valueOp :: (forall a. Op a => a -> a -> a) -> (Value -> Value -> Value)

(Обратите внимание, что для этого необходимо включить RankNTypes.) Этот тип принимает на вход бинарную функцию на a, которая работает для любой a, имеющей экземпляр Op. Итак, для первого случая вы можете написать:

valueOp op (A v) v' = A $ op v (toA v')

Входы и выходы по-прежнему имеют тип Value, но когда мы вызываем op, мы делаем это со значениями типа A, что нам и нужно. Два других случая следуют естественным образом:

valueOp op v (A v') = A $ op (toA v) v'
valueOp op v v' = B $ op (toB v) (toB v')

  • instance ImplicitlyConvertible Value - это просто куча шаблонного кода, от которого я хотел бы избавиться.
  • .

Вам действительно нужен экземпляр ImplicitlyConvertible для A, B и C? Если вы никогда не используете их независимо, то вы можете объединить их в экземпляр Value, что определенно сократит количество шаблонов. В этом случае, если у вас только один экземпляр (экземпляр Value), вы можете подумать о том, чтобы вообще избавиться от структуры class и просто определить функции toA :: Value -> Value и toB :: Value -> Value.

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


  • Тот факт, что toB фактически не реализован для A, кажется нечистым. Даже если он никогда не должен вызываться, я бы хотел избежать необходимости его реализации вообще.
  • .

Это должно поставить перед вами вопрос о вашей общей стратегии. Во многих отношениях вы сделали вещи приятно общими, но неясно, что это вам дало. В конце концов, если ваш единственный случай использования ImplicitlyConvertible находится в valueOp, действительно ли вам нужен новый класс только для одной функции? Если нет, то, возможно, вам следует включить экземпляры в определение самого valueOp? Возможно, в определении по-прежнему будет возникать ошибка "невозможно преобразовать A в B", но вы сможете доказать, что эта функция никогда не вызывалась, в отличие от вашего текущего кода, где любой может прийти и вызвать toB на значении A.

valueOp :: (forall a. Op a => a -> a -> a) -> (Value -> Value -> Value)
valueOp op x y = case (x,y) of
  (A v, v') -> A $ op v (toA v')
  (v, A v') -> A $ op (toA v) v'
  (v, v') -> B $ op (toB v) (toB v')
  где
    toA (A a) = a
    toA (B b) = сумма b
    toA (C c) = сумма c
    toB (A a) = ошибка "невозможно преобразовать A в B"
    toB (B b) = b
    toB (C c) = [fst c, snd c]

Альтернативно, если такое преобразование необходимо, вы могли бы определять его только тогда, когда это явно возможно:

class Convert x y where
  convert :: x -> y

экземпляр Convert Value A где
  convert (A a) = convert a
  convert (B b) = convert b
  преобразовать (C c) = преобразовать c

экземпляр Convert A A где
  преобразовать = id

экземпляр Преобразовать B A где
  преобразование = сумма

экземпляр Конвертировать C A где
  преобразование = сумма

экземпляр Конвертировать B B где
  преобразование = id

экземпляр Convert C B где
  преобразовать c = [fst c, snd c]


valueOp :: (forall a. Op a => a -> a -> a) -> (Value -> Value -> Value)
valueOp op (A v) v' = A $ op v (преобразовать v')
valueOp op v (A v') = A $ op (convert v) v'
valueOp op (B v) (B v') = B $ op v v'
valueOp op (B v) (C v') = B $ op v (convert v')
valueOp op (C v) (B v') = B $ op (convert v) v'
valueOp op (C v) (C v') = B $ op (convert v) (convert v')

Это требует перечисления всех возможных вариантов B и C в качестве входов в valueOp, что удручающе многословно, но вы можете спокойно отдыхать, зная, что все ваши функции полны.

2022 WebDevInsider