Какой хороший способ управлять невозможными состояниями в Elm? - PullRequest
2 голосов
/ 07 января 2020

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

Я пытаюсь построить что-то в Elm, которое использует основанную на графике структуру данных. Я создаю график с беглым / фабричным шаблоном, например:

sample : Result String MyThing
sample =
  MyThing.empty
    |> addNode 1 "bobble"
    |> addNode 2 "why not"
    |> addEdge 1 2 "some data here too"

Когда этот код возвращает Ok MyThing, тогда весь график был настроен согласованным образом, гарантировано, то есть все узлы и ребра иметь необходимые данные, и ребра для всех узлов фактически существуют.

Фактический код имеет более сложные данные, связанные с узлами и ребрами, но это не имеет значения для вопроса. Внутренне узлы и ребра хранятся в Dict Int element.

type alias MyThing =
  { nodes : Dict Int String
  , edges : Dict Int { from : Int, to : Int, label : String } 
  }

Теперь, у пользователей модуля, я хочу получить доступ к различным элементам графика. Но всякий раз, когда я получаю доступ к одному из узлов или ребер с помощью Dict.get, я получаю Maybe. Это довольно неудобно, потому что в силу моего конструкторского кода я знаю индексы существуют и c. Я не хочу загромождать исходный код Maybe и Result, когда я знаю , индексы в ребре существуют. В качестве примера:

getNodeTexts : Edge -> MyThing -> Maybe (String, String)
getNodeTexts edge thing =
  case Dict.get edge.from thing.nodes of
    Nothing ->
      --Yeah, actually this can never happen...
      Nothing
    Just fromNode -> case Dict.get edge.to thing.nodes of
      Nothing -> 
        --Again, this can never actually happen because the builder code prevents it.
        Nothing
      Just toNode ->
        Just ( fromNode.label, toNode.label )

Это просто много стандартного кода для обработки того, что я специально предотвратил в заводском коде. Но что еще хуже: теперь потребителю нужен дополнительный шаблонный код для обработки Maybe - потенциально не зная, что Maybe на самом деле никогда не будет Nothing. API - своего рода обман лжи для потребителя. Разве это не то, чего Элм пытается избежать? Сравните с гипотетическим, но неверным:

getNodeTexts : Edge -> MyThing -> (String, String)
getNodeTexts edge thing =
  ( Dict.get edge.from thing.nodes |> .label
  , Dict.get edge.to thing.nodes |> .label 
  )

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

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

Примечание: я также пытался использовать библиотеки drathier и elm-community elm-graph, но они не решают конкретный вопрос c. Они также опираются на Dict снизу, так что я получаю те же Maybe s.

Ответы [ 2 ]

3 голосов
/ 07 января 2020

Нет простого ответа на ваш вопрос. Я могу предложить один комментарий и предложение по кодированию.

Вы используете магию c слова "невозможное состояние", но, как указал OOBalance, вы можете создать невозможное состояние в своем моделировании. Нормальное значение «невозможного состояния» в Elm в точности связано с моделированием, например, когда вы используете два Bool s для представления 3 возможных состояний. В Elm вы можете использовать пользовательский тип для этого и не оставлять одну комбинацию bools в вашем коде.

Что касается вашего кода, вы можете уменьшить его длину (и, возможно, сложность) с помощью

getNodeTexts : Edge -> MyThing -> Maybe ( String, String )
getNodeTexts edge thing =
    Maybe.map2 (\ n1 n2  -> ( n1.label, n2.label ))
        (Dict.get edge.from thing.nodes)
        (Dict.get edge.to thing.nodes) 
3 голосов
/ 07 января 2020

Из вашего описания мне кажется, что эти состояния на самом деле не являются невозможными.

Давайте начнем с вашего определения MyThing:

type alias MyThing =
  { nodes : Dict Int String
  , edges : Dict Int { from : Int, to : Int, label : String } 
  }

Это псевдоним типа , а не тип - это означает, что компилятор будет принимать MyThing вместо {nodes : Dict Int String, edges : Dict Int {from : Int, to : Int, label : String}} и наоборот.

Поэтому вместо безопасного создания значения MyThing с использованием функций фабрики я могу написать:

import Dict
myThing = { nodes = Dict.empty, edges = Dict.fromList [(0, {from = 0, to = 1, label = "Edge 0"})] }

… и затем передайте myThing любой из ваших функций, ожидающих MyThing, даже если узлы, соединенные Edge 0 , не содержатся в myThing.nodes.

Это можно исправить, изменив MyThing на пользовательский тип:

type MyThing
    = MyThing { nodes : Dict Int String
              , edges : Dict Int { from : Int, to : Int, label : String } 
              }

… и выставив его, используя exposing (MyThing) вместо exposing (MyThing(..)). Таким образом, конструктор для MyThing не предоставляется, и код вне вашего модуля должен использовать фабричные функции для получения значения.

То же самое относится к Edge, который, как я предполагаю, определяется как :

type alias Edge =
    { from : Int, to : Int, label : String }

Если он не изменен на пользовательский тип, тривиально создать произвольные значения Edge:

type Edge
    = Edge { from : Int, to : Int, label : String }

Затем вам нужно будет открыть некоторые функции для получить Edge значений для передачи в функции типа getNodeTexts. Давайте предположим, что я получил MyThing и одно из его ребер:

myThing : MyThing
-- created using factory functions

edge : Edge
-- an edge of myThing

Теперь я создаю другое значение MyThing и передаю его getNodeTexts вместе с edge:

myOtherThing : MyThing
-- a different value of type MyThing

nodeTexts = getNodeTexts edge myOtherThing

Это должно вернуть Maybe.Nothing или Result.Err String, но определенно не (String, String) - ребро не принадлежит myOtherThing, поэтому нет гарантии, что его узлы содержатся в нем.

...