Приведение существующих типов для внутреннего DSL в F#

Учитывая DU для AST (очень простое дерево выражений)

type Expression =
  | Add of list<Expression>
  | Var of string
  | Val of float

Я хочу написать оператор + такой, чтобы я мог написать

let x = "s" + 2.0

и имеют

x = Add [(Var "s"); (Val 2.0)]

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

Мое реальное приложение — это аналогичный, но более крупный DU для настоящего AST. У меня F# 4.8.

То, что работает до сих пор,

let (+) a b =
    Add [a; b]
x = (Var "s") + (Val 2.0)

а тут еще "s" и "2.0" приходится заворачивать вручную. Я хочу избежать этой упаковки.

Я пробовал несколько других вещей:

Объявление расширений типов и интерфейса и использование как параметров статического типа, так и ограничений интерфейса:

Сначала интерфейсы

type IToExpression =
  abstract member ToExpression : Expression

type Expression with
  member this.ToExpression = this

type System.String with
  member this.ToExpression = Var this

type System.Double with
  member this.ToExpression = Val this

let (+++)
    (a : 'S when 'S :> IToExpression)
    (b : 'T when 'T :> IToExpression) =
    Add [a.ToExpression; b.ToExpression]

И то же самое со статически разрешенными параметрами типа.

let (++) a b =
  let a = (^t : (member ToExpression: Expression) a)
  let b = (^t : (member ToExpression: Expression) b)
  Add [a; b]

Редактировать: Но, как указано в этом ответе (и из-за моего небрежного копирования), этот подход требует дополнительной работы, чтобы даже добраться до реальной проблемы.

Но и ++, и +++ не проходят проверку типов в искомом выражении, т. е. в строках

let x = "s" ++ 2.0
let x = "s" +++ 2.0

я прочитал

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

Есть ли способ решить эту проблему?

Как создавать пользовательские общие типы в Python (50/100 дней Python)
Как создавать пользовательские общие типы в Python (50/100 дней Python)
Помимо встроенных типов, модуль типизации в Python предоставляет возможность определения общих типов, что позволяет вам определять типы, которые могут...
4
0
85
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Здесь есть несколько тонких проблем.

Во-первых, SRTP работают только с inline функциями. Это связано с тем, что они не могут быть скомпилированы в IL (поскольку IL не поддерживает такого рода ограничения), поэтому их необходимо разрешать во время компиляции. Статически. Вот почему они «статически разрешены». Ключевое слово inline позволяет компилятору сделать это.

let inline (+) a b = ...

Во-вторых, что это за универсальный тип ^t, на который вы ссылаетесь? Это тип чего? Я думаю, вы хотели, чтобы это был тип a и b, но вы не объявили его как таковой, так что это просто какой-то случайный общий тип, плавающий вокруг. Его нужно привязать к параметрам:

let inline (+) (a: ^t) (b: ^t) = ...

В-третьих, из ваших примеров похоже, что вы действительно имели в виду, что a и b должны быть разных типов, не так ли?

let inline (+) (a: ^a) (b: ^b) = ...

В-четвертых, методы расширения не учитываются для статически разрешенных типов, поэтому вы не можете определить ToExpression на String и float и ожидать, что это сработает. Обычный трюк состоит в том, чтобы вместо этого объявить все ваши методы в специальном классе, который существует только для хранения этих методов:

type ToExpressionStub() =
    static member ToExpression s = Var s
    static member ToExpression f = Val f

И тогда можно было бы ожидать, что это сработает:

let inline (+) (a: ^a) (b: ^b) =
  let a = (ToExpression : (static member ToExpression: ^a -> Expression) a)
  let b = (ToExpression : (static member ToExpression: ^b -> Expression) b)
  Add [a; b]

Но нет, не будет. Это связано с тем, что статически разрешенные ограничения не могут применяться к конкретным типам, а только к переменным типа, таким как ^a или ^b. Так что делать?

Ну, мы можем просто дать нашей функции дополнительный параметр, например:

let inline (+) (dummy: ^c) (a: ^a) (b: ^b) =
  let a = (^c : (static member ToExpression: ^a -> Expression) a)
  let b = (^c : (static member ToExpression: ^b -> Expression) b)
  Add [a; b]

И тогда нам пришлось бы передавать значение ToExpressionStub() при каждом вызове:

let x = (+) (ToExpressionStub()) "foo" 2.0

Это, конечно, очень неудобно, поэтому вместо этого мы добавим еще одну промежуточную функцию с тремя параметрами и вызовем ее из оператора (+):

let inline doAdd (dummy: ^c) (a: ^a) (b: ^b) =
  let a = (^c : (static member ToExpression: ^a -> Expression) a)
  let b = (^c : (static member ToExpression: ^b -> Expression) b)
  Add [a; b]

let inline (+) (a: ^a) (b: ^b) = doAdd (ToExpressionStub()) a b

Почти готово! Но это тоже не совсем работает: на строке let b = ... получаем предупреждение, что «Эта конструкция делает код менее универсальным...», а потом на сайтах использования получаем ошибку, что нельзя использовать ни string, ни float , в зависимости от того, что было раньше.

Происходит это по очень-очень непонятным причинам. Компилятор видит, что два ограничения имеют одинаковую форму и применяются к одному и тому же типу, и решает, что они должны быть одним и тем же ограничением, и, следовательно, ^a = ^b. Чтобы выйти из этого тупика, мы можем просто изменить форму ограничений, чтобы сделать их другими:

let inline doAdd (dummy: ^c) (a: ^a) (b: ^b) =
  let a = ((^a or ^c) : (static member ToExpression: ^a -> Expression) a)
  let b = ((^b or ^c) : (static member ToExpression: ^b -> Expression) b)
  Add [a; b]

Обратите внимание, что теперь они применяются к ^a or ^c и ^b or ^c соответственно, а не только к ^c. Это также имеет бонусный эффект: мы больше не ограничены только типами, которые мы перечислили в ToExpressionStub. Мы можем использовать любой тип, для которого определен собственный нерасширяющий метод ToExpression. Например, сам Expression:

type Expression =
  | Add of list<Expression>
  | Var of string
  | Val of float
  with
    static member ToExpression (e: Expression) = e

Вот и все! Теперь это работает:

> let x = 2.0 + "foo"
Add [Val 2.0; Var "foo"]

> let y = x + "bar"
Add [Add [Val 2.0; Var "foo"]; Var "bar"]

Наконец, чтобы уменьшить затраты на выделение памяти во время выполнения, я обычно использую одиночный экземпляр ToExpressionStub, а не создаю новый при каждом вызове:

type ToExpressionStub() = 
    static member val Value = ToExpressionStub()
    ...

let inline (+) (a: ^a) (b: ^b) = doAdd ToExpressionStub.Value a b

Суть в том, что да, вы можете это сделать, но, пожалуйста, хорошенько подумайте, стоит ли вам это делать.

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

Читая это, я вспомнил, как пытался изменить одну из этих библиотек задач, чтобы они интегрировались с оболочками Option и Result...

Sebastian Redl 21.12.2020 18:37

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

HTC 23.12.2020 21:27

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

HTC 23.12.2020 21:29

Я попробовал этот подход на своем полном примере и согласен: это не стоит затрат. Идея заключалась в том, чтобы обернуть хороший функциональный фреймворк (используя только основные конструкции ML) чем-то, что я мог бы использовать для быстрого написания материала в блокнотах Jupyther для создания более стабильного представления. Таким образом, я вижу, что чтение обслуживания вызывает меньше беспокойства, но проблемы с реализацией меня отучили. Тем не менее, его стоило изучить.

HTC 23.12.2020 21:32

Другие вопросы по теме