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

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

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

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

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)
}
  1. Каков наилучший способ создания «эквивалентного» кода Haskell для псевдокода OO, который я предоставил? Я хочу:

    • to be able to translate non-mutable classes with methods (which can have default arguments) to Haskell's code. (preserving laziness, so I do not want to use ugly IORefs and mimic mutable data structures)
    • не заставлять пользователя явно писать какие-либо типы, поэтому я могу использовать все доступные механизмы Haskell, чтобы разрешить автоматический вывод типов — например, с помощью Шаблон Haskell для автоматического создания экземпляров класса типов для заданных методов (и т. д.).
    • чтобы иметь возможность генерировать такой код с помощью моего компилятора без необходимости реализации моего собственного устройства вывода типов (или с помощью моего собственного устройства вывода типов, если нет другого решения)
    • код результата для создания быстрых двоичных файлов (хорошо оптимизированный при компиляции).
  2. Если предложенный ниже рабочий процесс является наилучшим из возможных, как мы можем исправить предложенный код 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 не может вывести неявные типы ранга выше 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 или у меня есть написать для этого свой собственный механизм вывода типов?

ОБНОВЛЕНИЕ

Решение, предоставленное 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 возвращать результат определенного типа на основе типа его аргумента?


person Wojciech Danilo    schedule 20.10.2013    source источник
comment
Я верю, что вам придется сделать вывод о типах методов самостоятельно. В противном случае просто измените имена, чтобы обеспечить пространство имен, и используйте автономные функции в качестве методов под капотом. Передайте запись полей. Это должно дать haskell гораздо больше шансов на вывод типов.   -  person Daniel Gratzer    schedule 21.10.2013
comment
Это прозвучит глупо, но... можете ли вы передать отдельную запись X для каждого вызова метода? например скомпилировать f в f x1 x2 = print (method1 x1 (1 :: Int) (2 :: Int)) >> print (method1 x2 "Hello" "World") и main в main = f cons_X cons_X или даже main = let x = cons_X in f x x.   -  person Daniel Wagner    schedule 21.10.2013
comment
Даже если это немного похоже на OO — использование cons для конструкторов сбивает с толку, аббревиатура слишком сильно занята списками Lisp.   -  person leftaroundabout    schedule 21.10.2013
comment
@leftaroundabout: обратите внимание, что код является сгенерированным кодом, и пользователь никогда его не увидит. Однако я изменю cons_Something на другое соглашение.   -  person Wojciech Danilo    schedule 21.10.2013
comment
@jozefg: в этом случае изменение имен не сработает. Рассмотрим следующий псевдокод: def test(x): x.f(). Я не знаю, какого типа x, когда набираю эту функцию, поэтому я не могу определить искаженное имя f.   -  person Wojciech Danilo    schedule 21.10.2013
comment
@DanielWagner: Это не глупо, это действительно интересная идея, но она все еще не работает. Если я напишу f сигнатуру как f x1 x2, то, конечно, я могу вызвать f x x из тела main, но это очень частный случай, когда x был определен в main. Идея не работает, когда f вызывается из другой функции. Например f_wrapper x = f x x, который дает ту же ошибку. Конечно, мы можем попробовать отслеживать переменные, чтобы каждая функция, использующая такую ​​функцию, продвигала эти аргументы в свою сигнатуру, но я не понимаю, как мы можем это сделать :(   -  person Wojciech Danilo    schedule 21.10.2013


Ответы (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 в 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")

Теперь мы можем создать несколько объектов ОО классов 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

Выгода!


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

(&) :: a -> (a -> b) -> b
x & f = f x
person fizruk    schedule 21.10.2013
comment
похоже, вы только что переместили подписи, которые просматривает OP, из f в класс - person aavogt; 22.10.2013