Создание методов, связанных с записями в Haskell

Я создаю ленивый, функциональный DSL, который позволяет пользователям определять непеременные структуры с помощью методов (что-то вроде классов из OO языки, но они не изменяемы). Я компилирую код этого языка в код Haskell.

Недавно у меня возникла проблема с этим документооборотом. Я не хочу заставлять пользователя писать явные типы, поэтому я хочу сильно использовать Hencell type inferencer. Проблема возникает, когда я переводю функцию, которая многократно вызывает полиморфный метод "объекта", передавая каждый раз разные типы аргументов, например здесь:

(псевдокод):

class X {
   def method1(a, b) {
       (a, b) // return
   }
}
def f(x) {
   print (x.method1(1,2))              // call method1 using Ints
   print (x.method1("hello", "world")) // call method1 using Strings
}

def main() {
   x = X() // constructor
   f(x)
}
  • Каков наилучший способ генерации "эквивалентного" кода Haskell псевдокода OO, который я предоставил? Я хочу:

    • чтобы иметь возможность переводить непеременные классы с помощью методов (которые могут иметь аргументы по умолчанию) для кода Haskell. (сохраняя лень, поэтому я не хочу использовать уродливые IORefs и имитировать изменяемые структуры данных).
    • не, чтобы заставить пользователя явно писать любые типы, поэтому я могу использовать все доступные механизмы Haskell для автоматического вывода типа - например, используя Template Haskell, чтобы автоматически генерировать экземпляры typeclass для данных методов (и т.д.).
    • чтобы иметь возможность генерировать такой код с моим компилятором, без необходимости реализации моего собственного типа inferencer (или с моим собственным методом inferencer, если нет другого решения)
    • код результата для создания быстрых двоичных файлов (хорошо оптимизируется при компиляции).
  • Если предложенный ниже рабочий процесс является наилучшим, как мы можем исправить предложенный код Haskell таким образом, чтобы работали как f con_X, так и f con_Y? (см. ниже)

Текущее состояние работы

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

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}

-- class and its constructor definition
data X a = X { _methodx1 :: a } deriving(Show)
con_X = X { _methodx1 = (\a b -> (a,b)) }

-- There can be other classes with "method1"
class F_method1 cls sig where
  method1 :: cls sig -> sig

instance F_method1 X a where
  method1 = _methodx1

f x = do
  print $ (method1 x) (1::Int) (2::Int)
  print $ (method1 x) ("Hello ") ("World")

main = do
  let x = con_X
  f x

Вышеприведенный код не работает, потому что Haskell не может выводить неявные типы rank выше 1, например тип f. После небольшого обсуждения #haskell irc было найдено частичное решение, а именно, мы можем перевести следующий псевдокод:

class X {
   def method1(a, b) {
       (a, b) // return
   }
}

class Y {
   def method1(a, b) {
       a // return
   }
}

def f(x) {
   print(x.method1(1, 2))
   print(x.method1("hello", "world"))
}

def main() {
   x = X()
   y = Y()
   f(x)
   f(y)
}

для кода Haskell:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-}


data Y a = Y { _methody1 :: a } deriving(Show)
data X a = X { _methodx1 :: a } deriving(Show)

con_X = X { _methodx1 = (\a b -> (a,b)) }
con_Y = Y { _methody1 = (\a b -> a) }

class F_method1 cls sig where
  method1 :: cls sig -> sig

instance F_method1 X a where
  method1 = _methodx1

instance F_method1 Y a where
  method1 = _methody1

f :: (F_method1 m (Int -> Int -> (Int, Int)),
      F_method1 m (String -> String -> (String, String)))
      => (forall a. (Show a, F_method1 m (a -> a -> (a,a))) => m (a -> a -> (a, a))) -> IO ()
f x = do
  print $ (method1 x) (1::Int) (2::Int)
  print $ (method1 x) ("Hello ") ("World")

main = do
  f con_X
  -- f con_Y

Этот код действительно работает, но только для типа данных X (поскольку он жестко закодировал возвращаемый тип method1 в сигнатуре f. Строка f con_Y не работает. Кроме того, есть ли способ автоматически сгенерировать подпись f или мне нужно написать свой собственный инкрементный тип для этого?

UPDATE

Решение, предоставляемое Crazy FIZRUK, действительно работает для этого конкретного случая, но используя existential data types, например data Printable = forall a. Show a => Printable a, принудительно применяет все методы с определенным именем (т.е. метод method1 "), чтобы иметь одинаковый тип результата во всех возможных классах, чего я не хочу достичь.

В следующем примере ясно показано, что я имею в виду:

(псевдокод):

class X {
   def method1(a, b) {
       (a, b) // return
   }
}

class Y {
   def method1(a, b) {
       a // return
   }
}

def f(x) {
   print(x.method1(1, 2))
   x.method1("hello", "world") // return
}

def main() {
   x = X()
   y = Y()
   print (f(x).fst())    // fst returns first tuple emenet and is not defined for string
   print (f(y).length()) // length returns length of String and is not defined for tuples
}

Можно ли перевести такой код в Haskell, разрешив f вернуть результат определенного типа в зависимости от типа его аргумента?

Ответ 1

Решение

Хорошо, так вы можете подражать желаемому поведению. Вам понадобятся два расширения: RankNTypes и ExistentialQuantification.

Сначала введите категории ранга 2 в X и Y. Поскольку это свойство метода класса (я имею в виду класс OO):

data X = X { _X'method :: forall a b. a -> b -> (a, b) }
data Y = Y { _Y'method :: forall a b. a -> b -> a }

Затем вам нужно указать, какие свойства имеют тип возврата "method". Это связано с тем, что при вызове method в f вы не знаете реализации класса, который используете. Вы можете либо связать тип возвращаемого значения с классом типов, либо, возможно, использовать Data.Dynamic (я не уверен в последнем). Я продемонстрирую первый вариант.

Я перенесу ограничение в экзистенциальный тип Printable:

data Printable = forall a. Show a => Printable a

instance Show Printable where
    show (Printable x) = show x

Теперь мы можем определить желаемый интерфейс, который мы будем использовать в сигнатуре типа f:

class MyInterface c where
    method :: forall a b. (Show a, Show b) => (a, b) -> c -> Printable

Важно, чтобы интерфейс также был полиморфным. Я поместил аргументы в кортеж, чтобы имитировать также обычный синтаксис ООП (см. Ниже).

Экземпляры для X и Y просты:

instance MyInterface X where
    method args x = Printable . uncurry (_X'method x) $ args

instance MyInterface Y where
    method args y = Printable . uncurry (_Y'method y) $ args

Теперь f можно просто написать:

f :: MyInterface c => c -> IO ()
f obj = do
    print $ obj & method(1, 2)
    print $ obj & method("Hello, ", "there")

Теперь мы можем создать некоторые объекты классов OO X и Y:

objX :: X
objX = X $ λa b -> (a, b)

objY :: Y
objY = Y $ λa b -> a

И запустите эту вещь!

main :: IO ()
main = do
    f objX
    f objY

Profit!


Вспомогательная функция для удобного синтаксиса:

(&) :: a -> (a -> b) -> b
x & f = f x