F#: Как представить конечную коллекцию со строгой типизацией?

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

type Planet = Mercury | Venus | Earth
type PlanetInfo = { Diameter: float }
let planets =
    Map [ Mercury, { Diameter = 100. }
          Venus, { Diameter = 200. }
          Earth, { Diameter = 300. } ]
let venusDiameter = planets.[Venus].Diameter

Положительные стороны этого метода:

  1. Есть ровно три Planet, как определено размеченным объединением.
  2. У нас есть весь набор на карте planets, которым можно манипулировать, повторять и т. д.
  3. planets.[Mars] вызовет ошибку, потому что Марс не Planet.

Но с другой стороны:

  1. Между союзом и картой не обязательно должно быть однозначное отображение. Необходимость упомянуть каждую планету дважды является недостатком. Вот еще один метод, который касается последнего пункта:
type Planet = { Name: string; Diameter: float }
let planets =
    [ { Name = "Mercury"; Diameter = 100. }
      { Name = "Venus"; Diameter = 200. }
      { Name = "Earth"; Diameter = 300. } ]
    |> List.map (fun e -> e.Name, e)
    |> Map
let venusDiameter = planets.["Venus"].Diameter

Итак, теперь каждая планета упоминается только в одном месте, но planets.["Mars"] не вызывает ошибки времени компиляции, потому что идентификаторы планет теперь печатаются строками.

Есть ли способ сделать это, который имеет все четыре хороших момента?


person CarbonFlambe    schedule 31.12.2020    source источник
comment
Для начала используйте функцию с совпадением вместо карты.   -  person Bent Tranberg    schedule 31.12.2020
comment
@BentTrenberg Разве это не помешает цели номер 2?   -  person CarbonFlambe    schedule 31.12.2020
comment
Может быть. На мой взгляд, код не должен быть написан таким образом — с самими данными, выраженными в виде типов. Кроме того, необходимость отражения изолированной части логики является признаком плохой архитектуры. Эти две проблемы идут рука об руку.   -  person Bent Tranberg    schedule 31.12.2020
comment
Иногда различие между данными и типами не так просто. Вы всегда можете абстрагироваться от вещей, чтобы они были все более и более общими, а спецификой были некоторые данные, загружаемые во время выполнения. Вы теряете некоторые гарантии времени компиляции и теряете некоторую простоту, но вы можете выиграть от возможности представлять более широкую область. Вы должны оценивать этот компромисс в каждой ситуации.   -  person Chechy Levas    schedule 31.12.2020
comment
В этом случае, если приложение конкретно связано с планетами, вращающимися вокруг Солнца, то я не вижу особой пользы в представлении планет в виде данных. Но если бы приложение было связано с вращением тел вокруг любого небесного объекта, то это должны были бы быть данные. Если вы уверены, что доменом всегда будут планеты вокруг Солнца, то зачем все усложнять? Но если это приложение, возможно, потребуется расширить в будущем, то, вероятно, лучше обобщить сейчас, чем пытаться обобщить завершенный проект позже.   -  person Chechy Levas    schedule 31.12.2020


Ответы (2)


Как насчет этого?

type Planet =
    |Mercury
    |Venus
    |Earth
    member this.Diameter =
        match this with
        |Mercury -> 100.
        |Venus -> 200.
        |Earth -> 300.

open Microsoft.FSharp.Reflection
let planets =
    FSharpType.GetUnionCases(typeof<Planet>)
    |> Array.map (fun case -> FSharpValue.MakeUnion(case, [||]) :?> Planet)
        
person Chechy Levas    schedule 31.12.2020
comment
Я надеялся избежать размышлений, но если в ближайшее время не появится лучшего ответа, я приму этот. Несмотря на свою многословность, она удовлетворяет четырем моим пожеланиям. - person CarbonFlambe; 31.12.2020
comment
Если вам нужно перечислить несколько подобных объединений, вы всегда можете создать для этого общую вспомогательную функцию. Просто замените Planet на 'T. - person Chechy Levas; 31.12.2020
comment
Отличная идея; спасибо за полезный ответ и комментарий. - person CarbonFlambe; 31.12.2020
comment
Я поддерживаю этот подход, но... На данный момент даже нет необходимости в карте. Если у вас есть планета, у вас уже есть диаметр. Карта не добавляет ценности в этот момент. - person Reed Copsey; 05.01.2021

Другой вариант — использовать тип Planet в качестве члена Name в типе PlanetInfo и инициализировать Map с помощью преобразования из списка:

module Planets =
    type Planet = 
    | Mercury
    | Venus
    | Earth

    type PlanetInfo = { Name: Planet; Diameter: float}

    let planets : PlanetInfo list = 
        [
            {Name = Mercury; Diameter = 100.}
            {Name = Venus; Diameter = 200.}
            {Name = Earth; Diameter = 300.}
        ]

    let planetsmap = planets |> List.map (fun pi -> pi.Name, pi) |> Map.ofList 

    
    planetsmap.[Mercury].Diameter

Этот подход не требует размышлений и предлагает проверку типов во время компиляции. Так что это почти то же самое, что и твой второй подход, Моника.

person Piotr Rodak    schedule 31.12.2020
comment
Достойное представление @Piotr, но я считаю, что оно больше похоже на мой первый подход: нет принудительного соблюдения соответствия между объединением и картой во время компиляции. т. е. можно было бы удалить элемент с карты. - person CarbonFlambe; 01.01.2021
comment
Просто добавьте модульный тест, чтобы выявить потенциальную проблему, и тогда это будет хорошим решением. - person Bent Tranberg; 01.01.2021