Выражение вычисления для построения графа сложных объектов

Учитывая следующие типы:

type Trip = {
  From: string
  To: string
}

type Passenger = {
   Name: string
   LastName: string
   Trips: Trip list
}

Я использую следующие конструкторы:

type PassengerBuilder() = 
  member this.Yield(_) = Passenger.Empty

  [<CustomOperation("lastName")>]
  member __.LastName(r: Passenger, lastName: string) = 
    { r with LastName = lastName }

  [<CustomOperation("name")>]
  member __.Name(r: Passenger, name: string) = 
    { r with Name = name }

type TripBuilder() = 
  member __.Yield(_) = Trip.Empty

  [<CustomOperation("from")>]
  member __.From(t: Trip, f: string) = 
    { t with From = f }

  // ... and so on

для создания записей типа Passenger, например:

let passenger = PassengerBuilder()
let trip = TripBuilder()

let p = passenger {
  name "john"
  lastName "doe"
}

let t = trip {
  from "Buenos Aires"
  to "Madrid"
}

как мне объединить PassengerBuilder и TripBuilder, чтобы добиться такого использования?

let p = passenger {
    name "John"
    lastName "Doe"
    trip from "Buenos Aires" to "Madrid"
    trip from "Madrid" to "Paris"
}

который возвращает запись Passenger, например:

{
   LastName = "Doe"
   Name = "John"
   Trips = [
       { From = "Buenos Aires"; To = "Madrid" }
       { From = "Madrid"; To = "Paris" }
   ]
}

person Federico Berasategui    schedule 05.03.2018    source источник
comment
Каким будет тип результата такого выражения?   -  person Fyodor Soikin    schedule 05.03.2018
comment
@FyodorSoikin извините, отредактировано.   -  person Federico Berasategui    schedule 05.03.2018
comment
Подождите, но тип Passenger объявляется не так. Вы хотите вернуть другой тип? Или вы хотите изменить определение Passenger?   -  person Fyodor Soikin    schedule 05.03.2018
comment
@FyodorSoikin извините, я мог что-то упустить, но да, тип Passenger имеет только Name, LastName и Trip list, что и показано в нижнем примере.   -  person Federico Berasategui    schedule 05.03.2018
comment
@FyodorSoikin Я не хочу менять тип, я хочу знать, как я могу объединить два компоновщика, чтобы я мог использовать TripBuilder для создания Trip и передавать их в PassengerBuilder, чтобы он назначал эти Trip в запись Passenger.   -  person Federico Berasategui    schedule 05.03.2018


Ответы (2)


Есть ли причина, по которой вы хотите использовать построитель вычислительных выражений? Исходя из вашего примера, не похоже, что вы пишете что-то похожее на вычисления. Если вам просто нужен хороший DSL для создания поездок, вы можете довольно легко определить что-то, что позволит вам писать:

let p = 
  passenger [
    name "John"
    lastName "Doe"
    trip from "Buenos Aires" towards "Madrid"
    trip from "Madrid" towards "Paris"
  ]

Это в значительной степени именно то, что вы просили, за исключением того, что он использует [ .. ] вместо { .. } (потому что он создает список преобразований). Я также переименовал to в towards, потому что to — это ключевое слово, и его нельзя переопределить.

Код для этого довольно легко написать и следовать:

let passenger ops = 
  ops |> List.fold (fun ps op -> op ps)
    { Name = ""; LastName = ""; Trips = [] } 

let trip op1 arg1 op2 arg2 ps = 
  let trip = 
    [op1 arg1; op2 arg2] |> List.fold (fun tr op -> op tr)
      { From = ""; To = "" }
  { ps with Trips = trip :: ps.Trips }

let name n ps = { ps with Name = n }
let lastName n ps = { ps with LastName = n }
let from n tp = { tp with From = n }
let towards n tp = { tp with To = n }

Тем не менее, я бы по-прежнему рассматривал возможность использования обычного синтаксиса записи F # - он не намного уродливее, чем этот. Единственным недостатком приведенной выше версии является то, что вы можете создавать пассажиров с пустыми именами и фамилиями, от чего вам мешает F#!

person Tomas Petricek    schedule 06.03.2018
comment
Мне очень нравится это решение. Я попробую. Я использовал вычислительные выражения, потому что хотел понять их и научиться их использовать, но вы правы в том, что здесь не выполняются никакие реальные вычисления. - person Federico Berasategui; 07.03.2018
comment
В итоге я полностью удалил CE и использовал этот подход. Спасибо Томас! - person Federico Berasategui; 07.03.2018
comment
@FedericoBerasategui Я рад, что вы нашли это полезным! Тем не менее, я полностью понимаю попытки сделать это с помощью CE, это весело :-). - person Tomas Petricek; 08.03.2018

Я не уверен, что это то, что вы хотели, но ничто не мешает вам создать новую операцию с именем trip на вашем PassengerBuilder:

  [<CustomOperation("trip")>]
  member __.Trip(r: Passenger, t: Trip) = 
    { r with Trips = t :: r.Trips }

а затем использовать его следующим образом:

let p = passenger {
    name "John"
    lastName "Doe"
    trip (trip { from "Buenos Aires"; to "Madrid" })
    trip (trip { from "Madrid"; to "Paris" })
}

Возможно, вы даже можете сделать его чище, полностью убрав TripBuilder:

let p = passenger {
    name "John"
    lastName "Doe"
    trip { From = "Buenos Aires"; To = "Madrid" }
    trip { From = "Madrid"; To = "Paris" }
}

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

person Fyodor Soikin    schedule 05.03.2018
comment
Думаю, этого достаточно, мне придется жить с повторением trip слова в trip (trip { ... }) - person Federico Berasategui; 05.03.2018
comment
Можете показать пример сборщика для второй версии? - person Federico Berasategui; 05.03.2018
comment
Точно такой же PassengerBuilder. Выражение { From = "a"; To = "b" } — это просто стандартный синтаксис F# для создания экземпляра Trip. Операция PassengerBuilder.trip принимает этот экземпляр в качестве аргумента. - person Fyodor Soikin; 05.03.2018