Как мне перевести класс типа Haskell на F#?

Я пытаюсь перевести Arrows основной библиотеки Haskell на F # (я думаю, что это хорошее упражнение для лучшего понимания Arrows и F #, и я мог бы использовать их в проекте, над которым я работаю.) Однако прямой перевод невозможно из-за разницы в парадигмах. Haskell использует классы типов для выражения этого материала, но я не уверен, какие конструкции F# лучше всего отображают функциональность классов типов с идиомами F#. У меня есть несколько соображений, но я решил, что лучше всего поднять их здесь и посмотреть, что считается наиболее близким по функциональности.

Для толпы tl;dr: Как преобразовать классы типов (идиома Haskell) в идиоматический код F#?

Для тех, кто принимает мое длинное объяснение:

Этот код из стандартной библиотеки Haskell является примером того, что я пытаюсь перевести:

class Category cat where
    id :: cat a a
    comp :: cat a b -> cat b c -> cat a c
class Category a => Arrow a where
    arr :: (b -> c) -> a b c
    first :: a b c -> a (b,d) (c,d)

instance Category (->) where
    id f = f
instance Arrow (->) where
    arr f = f
    first f = f *** id

Попытка 1: модули, простые типы, привязки Let

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

type Arrow<'a,'b> = Arrow of ('a -> 'b)

let arr f = Arrow f
let first f = //some code that does the first op

Это работает, но проигрывает полиморфизму, поскольку я не реализую категории и не могу легко реализовать более специализированные стрелки.

Попытка 1а: уточнение с помощью сигнатур и типов

Один из способов исправить некоторые проблемы с попыткой 1 — использовать файл .fsi для определения методов (чтобы упростить применение типов) и использовать некоторые простые настройки типов для специализации.

type ListArrow<'a,'b> = Arrow<['a],['b]>
//or
type ListArrow<'a,'b> = LA of Arrow<['a],['b]>

Но файл fsi нельзя повторно использовать (для обеспечения соблюдения типов связанных функций let) для других реализаций, а переименование/инкапсуляция типов сложны.

Попытка 2: Объектные модели и интерфейсы

Объясняя, что F# также создан для объектно-ориентированного программирования, возможно, иерархия типов — правильный способ сделать это.

type IArrow<'a,'b> =
    abstract member comp : IArrow<'b,'c> -> IArrow<'a,'c>
type Arrow<'a,'b>(func:'a->'b) = 
    interface IArrow<'a,'b> with
        member this.comp = //fun code involving "Arrow (fun x-> workOn x) :> IArrow"

Помимо того, насколько сложно заставить статические методы (такие как comp и другие операторы) работать как методы экземпляра, также необходимо явно повышать результаты. Я также не уверен, что эта методология все еще улавливает всю выразительность полиморфизма классов типов. Это также затрудняет использование вещей, которые ДОЛЖНЫ быть статическими методами.

Попытка 2а. Уточнение с помощью расширений типов

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

type IArrow<'a,'b> with
    static member (&&&) f = //code to do the fanout operation

Ах, но это заставляет меня использовать один метод для всех типов IArrow. Если бы я хотел немного другого (&&&) для ListArrows, что я мог бы сделать? Я еще не пробовал этот метод, но я думаю, что могу скрыть (&&&) или, по крайней мере, предоставить более специализированную версию, но я чувствую, что не могу принудительно использовать правильный вариант.

Помогите мне

Так что мне здесь делать? Я чувствую, что OO должен быть достаточно мощным, чтобы заменить классы типов, но я не могу понять, как это сделать в F #. Были ли какие-либо из моих попыток близкими? Являются ли какие-либо из них «настолько хорошими, насколько это возможно», и этого должно быть достаточно?


person CodexArcanum    schedule 27.10.2010    source источник
comment
Как перевести классы типов (идиома Haskell) в идиоматический код F#? Плохая идея. Вам следует взглянуть на проблему, которую вы пытались решить, а не на решение, в котором использовались классы типов, и выяснить, как решить ее с помощью функции, которую предоставляет F#.   -  person J D    schedule 29.10.2010
comment
@Jon Harrop В этом и суть вопроса. Haskell решает десятки проблем с помощью классов типов, и я хотел знать, какие есть альтернативы F# для решения аналогичного класса проблем. Кроме того, порт Arrow не предназначен для решения каких-либо проблем, просто я подумал, что было бы интересно узнать больше об активности Arrows.   -  person CodexArcanum    schedule 29.10.2010
comment
Тогда я думаю, что было бы очень полезно, если бы вы могли объяснить некоторые проблемы, которые решают классы типов, и спросить людей, как бы они решили те же проблемы в F#.   -  person J D    schedule 30.10.2010
comment
@Gustavo Я был бы рад поменяться ответами (давно я не слишком много думал об этом вопросе), но я не полностью следую вашему коду. Решает ли ваш метод проблемы, затронутые в первом вопросе о полиморфизме высших родов? Например, если бы у меня была стрелка, содержащая типы монад, все это работало бы?   -  person CodexArcanum    schedule 07.06.2012
comment
@CodexArcanum Да, вроде того. Выведенные статические ограничения требуют, чтобы в универсальном классе существовали некоторые статические методы. Он работает на уровне метода, поэтому он будет работать не только с монадами, но и с каждым классом, имеющим Return и Bind, и в конечном итоге может потребоваться только Return (как в случае с Id).   -  person Gus    schedule 11.06.2012


Ответы (2)


Вот подход, который я использую для моделирования классов типов (из http://code.google.com/p/fsharp-typeclasses/ ).

В вашем случае для Arrows может быть что-то вроде этого:

let inline i2 (a:^a,b:^b     ) =                                                      
    ((^a or ^b      ) : (static member instance: ^a* ^b     -> _) (a,b  ))
let inline i3 (a:^a,b:^b,c:^c) =                                                          
    ((^a or ^b or ^c) : (static member instance: ^a* ^b* ^c -> _) (a,b,c))

type T = T with
    static member inline instance (a:'a      ) = 
        fun x -> i2(a   , Unchecked.defaultof<'r>) x :'r
    static member inline instance (a:'a, b:'b) = 
        fun x -> i3(a, b, Unchecked.defaultof<'r>) x :'r


type Return = Return with
    static member instance (_Monad:Return, _:option<'a>) = fun x -> Some x
    static member instance (_Monad:Return, _:list<'a>  ) = fun x  ->    [x]
    static member instance (_Monad:Return, _: 'r -> 'a ) = fun x _ ->    x
let inline return' x = T.instance Return x

type Bind = Bind with
    static member instance (_Monad:Bind, x:option<_>, _:option<'b>) = fun f -> 
        Option.bind  f x
    static member instance (_Monad:Bind, x:list<_>  , _:list<'b>  ) = fun f -> 
        List.collect f x
    static member instance (_Monad:Bind, f:'r->'a, _:'r->'b) = fun k r -> k (f r) r
let inline (>>=) x (f:_->'R) : 'R = T.instance (Bind, x) f
let inline (>=>) f g x    = f x >>= g

type Kleisli<'a, 'm> = Kleisli of ('a -> 'm)
let runKleisli (Kleisli f) = f

type Id = Id with
    static member        instance (_Category:Id, _: 'r -> 'r     ) = fun () -> id
    static member inline instance (_Category:Id, _:Kleisli<'a,'b>) = fun () ->
        Kleisli return'
let inline id'() = T.instance Id ()

type Comp = Comp with
    static member        instance (_Category:Comp,         f, _) = (<<) f
    static member inline instance (_Category:Comp, Kleisli f, _) =
        fun (Kleisli g) -> Kleisli (g >=> f)

let inline (<<<) f g = T.instance (Comp, f) g
let inline (>>>) g f = T.instance (Comp, f) g

type Arr = Arr with
    static member        instance (_Arrow:Arr, _: _ -> _) = fun (f:_->_) -> f
    static member inline instance (_Arrow:Arr, _:Kleisli<_,_>) = 
        fun f -> Kleisli (return' <<< f)
let inline arr f = T.instance Arr f

type First = First with
    static member        instance (_Arrow:First, f, _: 'a -> 'b) = 
        fun () (x,y) -> (f x, y)
    static member inline instance (_Arrow:First, Kleisli f, _:Kleisli<_,_>) =
        fun () -> Kleisli (fun (b,d) -> f b >>= fun c -> return' (c,d))
let inline first f = T.instance (First, f) ()

let inline second f = let swap (x,y) = (y,x) in arr swap >>> first f >>> arr swap
let inline ( *** ) f g = first f >>> second g
let inline ( &&& ) f g = arr (fun b -> (b,b)) >>> f *** g

Использование:

> let f = Kleisli (fun y -> [y;y*2;y*3]) <<< Kleisli ( fun x -> [ x + 3 ; x * 2 ] ) ;;
val f : Kleisli<int,int list> = Kleisli <fun:f@4-14>

> runKleisli f <| 5 ;;
val it : int list = [8; 16; 24; 10; 20; 30]

> (arr (fun y -> [y;y*2;y*3])) 3 ;;
val it : int list = [3; 6; 9]

> let (x:option<_>) = runKleisli (arr (fun y -> [y;y*2;y*3])) 2 ;;
val x : int list option = Some [2; 4; 6]

> ( (*) 100) *** ((+) 9)   <| (5,10) ;;
val it : int * int = (500, 19)

> ( (*) 100) &&& ((+) 9)   <| 5 ;;
val it : int * int = (500, 14)

> let x:List<_>  = (runKleisli (id'())) 5 ;;
val x : List<int> = [5]

Примечание: используйте id'() вместо id

Обновление: для компиляции этого кода требуется F# 3.0, в противном случае вот версия F# 2.0.

И вот подробное объяснение этого метода, который является типобезопасным, расширяемым и, как вы можете видеть, работает даже с некоторыми классами типов более высокого типа.

person Gus    schedule 08.12.2011
comment
@GustavoGuerra Функция i3 не будет компилироваться в F# 2.0 из-за ошибки парсера. Есть обходной путь с использованием операторов, но код становится менее читаемым. - person Gus; 16.08.2013
comment
[ nut-cracker.com.ar/index.php ] (подробное объяснение ) не работает. - person Narfanar; 02.10.2015

Мой краткий ответ:

ООП недостаточно мощен, чтобы заменить классы типов.

Самый простой перевод — передать словарь операций, как в одной типичной реализации класса типов. То есть, если класс типов Foo определяет три метода, затем определите тип класса/записи с именем Foo, а затем измените функции

Foo a => yadda -> yadda -> yadda

к функциям как

Foo -> yadda -> yadda -> yadda

и на каждом сайте вызова вы знаете конкретный «экземпляр» для передачи в зависимости от типа на сайте вызова.

Вот краткий пример того, что я имею в виду:

// typeclass
type Showable<'a> = { show : 'a -> unit; showPretty : 'a -> unit } //'

// instances
let IntShowable = 
    { show = printfn "%d"; showPretty = (fun i -> printfn "pretty %d" i) }
let StringShowable = 
    { show = printfn "%s"; showPretty = (fun s -> printfn "<<%s>>" s) }

// function using typeclass constraint
// Showable a => [a] -> ()
let ShowAllPretty (s:Showable<'a>) l = //'
    l |> List.iter s.showPretty 

// callsites
ShowAllPretty IntShowable [1;2;3]
ShowAllPretty StringShowable ["foo";"bar"]

Смотрите также

https://web.archive.org/web/20081017141728/http://blog.matthewdoig.com/?p=112

person Brian    schedule 27.10.2010
comment
Хммм... Мне кажется, я видел нечто подобное в отношении класса F# Matrix, где у него есть словарь «Тип, интерфейсы, представленные как объекты», который сообщает матрице, какие вспомогательные классы, реализующие интерфейс, использовать для математических вычислений над типом, который был в Матрице. - person CodexArcanum; 27.10.2010
comment
Итак, если бы я хотел продублировать класс типа Display, я мог бы сделать это как: val show : Type -> IDisplay, затем иметь let Showable = Dict<Type, obj> и, наконец, реализовать интерфейс IDisplay для моего вспомогательного типа Foo, добавить тип Foo и вспомогательный метод в словарь, а затем использовать метод show поднять помощника в словаре и вызвать правильный метод? - person CodexArcanum; 27.10.2010
comment
Кроме того, мне кажется забавным, что, когда я впервые столкнулся с Haskell, я подумал, что классы типов — это дрянные интерфейсы с плохой инкапсуляцией; и теперь я начинаю думать, что объекты - это дерьмовые классы типов с плохим полиморфизмом. Они были правы, когда я прочитал, что изучение ФП погубит вас для ООП. - person CodexArcanum; 27.10.2010
comment
Ах, я видел этот пост в блоге раньше, но не читал его хорошо, пора это исправить, я полагаю. Спасибо за ссылку. Также вот сообщение в блоге, которое я упомянул о матрицах: fdatamining.blogspot.com/2010/03/ Он также рассказывает об интерфейсе INumerics. И да, ваша ссылка правильная, монады (и стрелки) намного менее полезны, если вы не можете правильно абстрагировать основные операции над ними. - person CodexArcanum; 27.10.2010
comment
Этот метод не так уж плох для такого класса типов, как Show. Однако я не видел хороших методов, когда требуются более высокие типы (например, в исходном вопросе cat относится к типу * => * => *, который не может быть четко представлен в .NET). - person kvb; 27.10.2010
comment
Я не думаю, что эта техника может работать для типов более высокого рода. Система типов .NET не поддерживает полиморфизм более высокого типа, такой как Haskell. - person snk_kid; 27.10.2010
comment
Нет, метод, представленный Брайаном, довольно быстро сломается, когда вы начнете добавлять типы, которые сами по себе являются универсальными (более высокого типа). Кажется, единственным выходом является метод поиска по словарю, если требуется полиморфизм класса типов, или просто ручное выписывание всего с большим количеством дублирований, чтобы подключить всю функциональность. - person CodexArcanum; 28.10.2010