Как реализовать «эффективную обобщенную кратность» в F#?

В документе Мартина и др. я прочитал об эффективных обобщенных свертках для вложенных типов данных. В статье говорится о Haskell, и я хочу попробовать его на F#.

Пока мне удалось следовать примеру Nest, включая реализацию gfold.

type Pair<'a> = 'a * 'a
type Nest<'a> = Nil | Cons of 'a * Nest<Pair<'a>>

let example =
    Cons(1,
        Cons((2, 3),
            Cons(((4, 5), (6, 7)),
                Nil
            )
        )
    )

let pair (f:'a -> 'b) ((a, b):Pair<'a>) : Pair<'b> = f a, f b

let rec nest<'a, 'r> (f:'a -> 'r) : Nest<'a> -> Nest<'r> = function
    | Nil -> Nil
    | Cons(x, xs) -> Cons(f x, nest (pair f) xs)

//val gfold : e:'r -> f:('a * 'r -> 'r) -> g:(Pair<'a> -> 'a) -> _arg1:Nest<'a> -> 'r
let rec gfold e f g : Nest<'a> -> 'r = function
    | Nil -> e
    | Cons(x, xs) ->
        f(x, gfold e f g (nest g xs))

let uncurry f (a, b) = f a b

let up = uncurry (+)

let sum = example |> gfold 0 up up

К сожалению, gfold имеет квадратичную сложность, поэтому авторы придумали efold. Как вы, наверное, догадались, это тот, который я не мог заставить работать. После того, как я возился со многими аннотациями типов, я придумал эту версию, в которой осталась только крошечная волнистая линия:

let rec efold<'a, 'b, 'r> (e:'r) (f:'a * 'r -> 'r) (g:(Pair<'a> -> Pair<'a>) -> 'a -> 'a) (h:_) (nest:Nest<'a>) : 'r =
    match nest with
    | Nil -> e
    | Cons(x, xs) -> f(h x, efold e f g ((g << pair) h) xs)
                                                        ^^

Единственный оставшийся неуказанный тип — это тип h. Компилятор выводит val h : ('a -> 'a), но я думаю, что должны быть разные типы.

Предоставленное сообщение об ошибке читает

Несоответствие типа ошибки. Ожидание
Nest‹'a>
, но получено
Nest‹Pair‹'a>>
Результирующий тип будет бесконечным при объединении ''a' и 'Pair''a>'

При правильном типе h ошибка должна исчезнуть. Но я недостаточно понимаю Haskell, чтобы перевести его на F#.

См. также это обсуждение о возможной опечатке в бумага.


Обновление: вот что я понимаю из ответа kvb:

Таким образом, h преобразует тип ввода в промежуточный тип, как в обычном сверте, где аккумулятор может быть другого типа. Затем g используется для уменьшения двух промежуточных типизированных значений до одного, в то время как f получает промежуточный тип и тип ввода для создания выходных типизированных значений. Конечно, e также относится к этому типу вывода.

h действительно напрямую применяется к значениям, встречающимся во время рекурсии. g, с другой стороны, используется только для того, чтобы сделать h применимым к более глубоким типам.

Просто взгляните на первые f примеры, сами по себе они, кажется, не делают много работы, кроме применения h и подпитки рекурсии. Но в изощренном подходе я вижу, что это самое важное. что выходит, т.е. это рабочая лошадка.

Это примерно так?


person primfaktor    schedule 17.11.2016    source источник
comment
Возможно, я ошибаюсь, так как кое-что из этого выходит за рамки моего понимания, но я все еще думаю, что есть потребность в классах типов или чем-то подобном тому, что существует в Haskell, но не в F#.   -  person Sehnsucht    schedule 17.11.2016
comment
@Sehnsucht - не классы типов, а типы более высокого порядка.   -  person kvb    schedule 17.11.2016
comment
@kvb спасибо, я был почти уверен, что использовал неправильное слово (рад, что я также поставил или что-то в этом роде)   -  person Sehnsucht    schedule 17.11.2016
comment
@primfaktor - я существенно обновил свой ответ, чтобы показать более общую кодировку, которая может вас заинтересовать.   -  person kvb    schedule 18.11.2016
comment
@primfaktor может быть способ добиться этого, используя FsControl в безопасном типе, статически во время компиляции стесненный способ. Я посмотрю, смогу ли я что-нибудь придумать. Кажется, это интересная задача для решения.   -  person Gus    schedule 18.11.2016
comment
Я кратко ответил на ваш обновленный вопрос.   -  person kvb    schedule 18.11.2016
comment
Я только что нашел это и подумал, что это как-то применимо здесь. Но то, что я понимаю, действительно в основном бананы.   -  person primfaktor    schedule 19.11.2016


Ответы (1)


Правильное определение efold в Haskell выглядит примерно так:

efold :: forall n m b.
    (forall a. n a)->
    (forall a.(m a, n (Pair a)) -> n a)->
    (forall a.Pair (m a) -> m (Pair a))->
    (forall a.(a -> m b) -> Nest a -> n b) 
efold e f g h Nil = e 
efold e f g h (Cons (x,xs)) = f (h x, efold e f g (g . pair h) xs

Это не может быть переведено в F# в полной мере, потому что n и m являются «типами более высокого порядка» — это конструкторы типов, создающие тип при наличии аргумента, которые не поддерживаются в F# (и не имеют четкого представления в .СЕТЬ).

Интерпретация

Ваше обновление спрашивает, как интерпретировать аргументы в фолд. Возможно, самый простой способ увидеть, как работает сгиб, — это рассмотреть, что происходит, когда вы применяете сгиб к вашему примеру. Вы получите что-то вроде этого:

efold e f g h example ≡
    f (h 1, f ((g << pair h) (2, 3), f ((g << pair (g << pair h)) ((4,5), (6,7)), e)))

Таким образом, h отображает значения в тип, который может служить первым аргументом f. g используется для применения h к более глубоко вложенным парам (чтобы мы могли перейти от использования h в качестве функции типа a -> m b к Pair a -> m (Pair b), Pair (Pair a) -> m (Pair (Pair b)) и т. д.), а f многократно применяется вверх по корешку, чтобы объединить результаты h с результаты вложенных вызовов f. Наконец, e используется ровно один раз, чтобы служить семенем наиболее глубоко вложенного вызова f.

Я думаю, что это объяснение в основном согласуется с тем, что вы сделали вывод. f, безусловно, имеет решающее значение для объединения результатов различных слоев. Но g тоже имеет значение, так как оно говорит вам, как комбинировать части внутри слоя (например, при суммировании узлов необходимо суммировать левые и правые вложенные суммы; если вы хотите использовать сгиб для создания нового гнезда, где значения на каждом уровне меняются местами по сравнению с входными значениями, вы должны использовать g, который выглядит примерно как fun (a,b) -> b,a).

Простой подход

Одним из вариантов является создание специализированных реализаций efold для каждой интересующей вас пары n, m. Например, если мы хотим просуммировать длины списков, содержащихся в Nest, тогда n _ и m _ оба будут равны int. Мы можем немного обобщить на случай, когда n _ и m _ не зависят от своих аргументов:

let rec efold<'n,'m,'a> (e:'n) (f:'m*'n->'n) (g:Pair<'m> -> 'm) (h:'a->'m) : Nest<'a> -> 'n = function
| Nil -> e
| Cons(x,xs) -> f (h x, efold e f g (g << (pair h)) xs)

let total = efold 0 up up id example

С другой стороны, если n и m действительно используют свои аргументы, вам нужно будет определить отдельную специализацию (плюс вам может понадобиться создать новые типы для каждого полиморфного аргумента, так как кодирование F# более высокого ранга< /em> неудобен). Например, чтобы собрать значения гнезда в список, вам нужно n 'a = list<'a> и m 'b = 'b. Затем вместо определения новых типов для типа аргумента e мы можем заметить, что единственным значением типа forall 'a.list<'a> является [], поэтому мы можем написать:

type ListIdF =
    abstract Apply : 'a * list<Pair<'a>> -> list<'a>

type ListIdG =
    abstract Apply : Pair<'a> -> Pair<'a>

let rec efold<'a,'b> (f:ListIdF) (g:ListIdG) (h:'a -> 'b) : Nest<'a> -> list<'b> = function
| Nil -> []
| Cons(x,xs) -> f.Apply(h x, efold f g (pair h >> g.Apply) xs)

let toList n = efold { new ListIdF with member __.Apply(a,l) = a::(List.collect (fun (x,y) -> [x;y]) l) } { new ListIdG with member __.Apply(p) = p } id n

Сложный подход

Хотя F# напрямую не поддерживает типы более высокого типа, оказывается, что их можно имитировать достаточно точным образом. Это подход, используемый библиотекой Higher. Вот как будет выглядеть его минимальная версия.

Мы создаем тип App<'T,'a>, который будет представлять некоторое приложение типа T<'a>, но где мы создадим фиктивный сопутствующий тип, который может служить аргументом первого типа для App<_,_>:

type App<'F, 'T>(token : 'F, value : obj) = 
    do
        if obj.ReferenceEquals(token, Unchecked.defaultof<'F>) then
            raise <| new System.InvalidOperationException("Invalid token")

    // Apply the secret token to have access to the encapsulated value
    member self.Apply(token' : 'F) : obj =
        if not (obj.ReferenceEquals(token, token')) then
            raise <| new System.InvalidOperationException("Invalid token")
        value 

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

// App<Const<'a>, 'b> represents a value of type 'a (that is, ignores 'b)
type Const<'a> private () =
    static let token = Const ()
    static member Inj (value : 'a) =
        App<Const<'a>, 'b>(token, value)
    static member Prj (app : App<Const<'a>, 'b>) : 'a =
        app.Apply(token) :?> _

// App<List, 'a> represents list<'a>
type List private () = 
    static let token = List()
    static member Inj (value : 'a list) =
        App<List, 'a>(token, value)
    static member Prj (app : App<List, 'a>) : 'a list =
        app.Apply(token) :?> _

// App<Id, 'a> represents just a plain 'a
type Id private () =
    static let token = Id()
    static member Inj (value : 'a) =
        App<Id, 'a>(token, value)
    static member Prj (app : App<Id, 'a>) : 'a =
        app.Apply(token) :?> _

// App<Nest, 'a> represents a Nest<'a>
type Nest private () =
    static let token = Nest()
    static member Inj (value : Nest<'a>) =
        App<Nest, 'a>(token, value)
    static member Prj (app : App<Nest, 'a>) : Nest<'a> =
        app.Apply(token) :?> _

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

// forall a. n a
type E<'N> =
    abstract Apply<'a> : unit -> App<'N,'a>

// forall a.(m a, n (Pair a)) -> n a)
type F<'M,'N> =
    abstract Apply<'a> : App<'M,'a> * App<'N,'a*'a> -> App<'N,'a>

// forall a.Pair (m a) -> m (Pair a))
type G<'M> =
    abstract Apply<'a> : App<'M,'a> * App<'M,'a> -> App<'M,'a*'a>

так что сгиб просто:

let rec efold<'N,'M,'a,'b> (e:E<'N>) (f:F<'M,'N>) (g:G<'M>) (h:'a -> App<'M,'b>) : Nest<'a> -> App<'N,'b> = function
| Nil -> e.Apply()
| Cons(x,xs) -> f.Apply(h x, efold e f g (g.Apply << pair h) xs)

Теперь, чтобы вызвать efold, нам нужно добавить несколько вызовов различных методов Inj и Prj, но в остальном все выглядит так, как мы и ожидали:

let toList n = 
    efold { new E<_> with member __.Apply() = List.Inj [] } 
          { new F<_,_> with member __.Apply(m,n) = Id.Prj m :: (n |> List.Prj |> List.collect (fun (x,y) -> [x;y])) |> List.Inj }
          { new G<_> with member __.Apply(m1,m2) = (Id.Prj m1, Id.Prj m2) |> Id.Inj }
          Id.Inj
          n
    |> List.Prj

let sumElements n =
    efold { new E<_> with member __.Apply() = Const.Inj 0 }
          { new F<_,_> with member __.Apply(m,n) = Const.Prj m + Const.Prj n |> Const.Inj }
          { new G<_> with member __.Apply(m1,m2) = Const.Prj m1 + Const.Prj m2 |> Const.Inj }
          Const.Inj
          n
    |> Const.Prj

let reverse n = 
    efold { new E<_> with member __.Apply() = Nest.Inj Nil }
          { new F<_,_> with member __.Apply(m,n) = Cons(Id.Prj m, Nest.Prj n) |> Nest.Inj }
          { new G<_> with member __.Apply(m1,m2) = (Id.Prj 2, Id.Prj m1) |> Id.Inj }
          Id.Inj
          n
    |> Nest.Prj

Надеюсь, схема здесь ясна: в каждом объектном выражении метод приложения проецирует каждый аргумент, оперирует с ним, а затем вводит результат обратно в тип App<_,_>. С некоторой магией inline мы можем сделать этот вид еще более последовательным (за счет нескольких аннотаций типа):

let inline (|Prj|) (app:App< ^T, 'a>) = (^T : (static member Prj : App< ^T, 'a> -> 'b) app)
let inline prj (Prj x) = x
let inline inj x = (^T : (static member Inj : 'b -> App< ^T, 'a>) x)

let toList n = 
    efold { new E<List> with member __.Apply() = inj [] } 
          { new F<Id,_> with member __.Apply(Prj m, Prj n) = m :: (n |> List.collect (fun (x,y) -> [x;y])) |> inj }
          { new G<_> with member __.Apply(Prj m1,Prj m2) = (m1, m2) |> inj }
          inj
          n
    |> prj

let sumElements n =
    efold { new E<Const<_>> with member __.Apply() = inj 0 }
          { new F<Const<_>,_> with member __.Apply(Prj m, Prj n) = m + n |> inj }
          { new G<_> with member __.Apply(Prj m1,Prj m2) = m1 + m2 |> inj }
          inj
          n
    |> prj

let reverse n = 
    efold { new E<_> with member __.Apply() = Nest.Inj Nil }
          { new F<Id,_> with member __.Apply(Prj m,Prj n) = Cons(m, n) |> inj }
          { new G<_> with member __.Apply(Prj m1,Prj m2) = (m2, m1) |> inj }
          inj
          n
    |> prj
person kvb    schedule 17.11.2016
comment
Вау, спасибо! Я уже с трудом понял, о чем вообще h. Я обновил вопрос, чтобы убедиться, что я не ошибаюсь. :) - person primfaktor; 18.11.2016
comment
Поскольку вы ответили на все мои недавние вопросы, вы, вероятно, уже догадались, к чему я клоню. Правильно ли я чувствую, что не смогу предоставить общее дерево пальцев? для пользователя? - person primfaktor; 19.11.2016
comment
Это, конечно, вызовет другой вопрос. Я с удовольствием напишу это, чтобы вы могли заработать больше очков. :) - person primfaktor; 19.11.2016
comment
@primfaktor - Вы должны уметь применять любой из этих подходов и к деревьям пальцев. Но я ожидаю, что большинство пользователей предпочтут не использовать складку, раскрывающую большую часть внутренней структуры дерева, а предпочли бы складку, сигнатура которой соответствует Seq.fold (например, fold : ('a -> 'b -> 'a) -> 'a -> FT<'b> -> 'a), и в этом случае вам не нужно так много сложных механизмов (хотя вам нужна полиморфная рекурсия). - person kvb; 23.11.2016