也许您可以提供帮助。我是Elm的初学者,并且遇到了相当平凡的问题。我对Elm感到非常兴奋,并且在较小的事情上取得了相当的成功,所以现在我尝试了一些更复杂的事情,但似乎还是无法解决。
我正在尝试在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
。这很不方便,因为借助我的构造函数代码,我know索引存在,等等。当我know一个索引时,我不想用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
实际上会是[[never是Nothing
。该API对消费者而言是一种谎言。这不是Elm想要避免的事情吗?与假设但不正确的比较:
getNodeTexts : Edge -> MyThing -> (String, String)
getNodeTexts edge thing =
( Dict.get edge.from thing.nodes |> .label
, Dict.get edge.to thing.nodes |> .label
)
一种替代方案是不使用Int
ID,而是使用实际数据-但由于连接器可能有许多边缘,因此更新事情变得非常繁琐。没有通过Int
解耦来管理状态似乎不是一个好主意。我觉得必须使用不透明的ID类型解决这个难题,但我只是看不到。我将不胜感激任何指针。
注意:我也尝试同时使用
drathier
和elm-community
榆木图库,但是它们没有解决特定的问题。它们也依赖于下面的Dict
,所以我最终得到相同的Maybe
。
您使用了魔术词“不可能的状态”,但是正如OOBalance指出的那样,您可以在建模中创建不可能的状态。 Elm中“不可能的状态”的正常含义恰好与建模有关,例如当您使用两个Bool
表示3种可能的状态时。在Elm中,您可以为此使用自定义类型,而不必在代码中保留布尔值的一种组合。
至于您的代码,您可以通过以下方式减少其长度(甚至可以减少复杂性)
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)
让我们开始定义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
,因此不能保证其节点包含在其中。