可扩展记录和高阶函数

时间:2017-11-16 04:03:41

标签: compiler-errors functional-programming elm higher-order-functions records

我想为具有类型为id的{​​{1}}字段的记录指定类型。所以,我使用可扩展记录。类型如下:

Uuid

到目前为止,这么好。但后来我为这种类型创建了一个type alias WithId a = { a | id : Uuid } - 即Json.Encoder类型的函数。我需要一种方法来编码底层值,所以我想采用类型为WithId a -> Value的第一个参数。由于我知道a -> Value是一个记录,我可以放心地假设它被编码为JSON对象,即使Elm没有公开a的数据类型。

但是,当我创建这样一个函数时,我得到一个编译错误:

Value

我很困惑。不是 The argument to function `encoder` is causing a mismatch. 27| encoder a ^ Function `encoder` is expecting the argument to be: a But it is: WithId a Hint: Your type annotation uses type variable `a` which means any type of value can flow through. Your code is saying it CANNOT be anything though! Maybe change your type annotation to be more specific? Maybe the code has a problem? More at: <https://github.com/elm-lang/elm-compiler/blob/0.18.0/hints/type-annotations.md> Detected errors in 1 module. 类型的某些内容将包含WithId a的所有字段,因此a不应该WithId a通过a结构打字?

我错过了什么?为什么不是type alias的类型别名,允许我使用WithId a作为WithId a的实例,即使它被定义为a

附录

我在下面标出了一个答案(相当于&#34;你不能这样做,但这是你应该做的事。&#34 ;),但我还是有点不满意。我想我对术语{a|...}的使用感到困惑。我的理解是记录总是type alias,因为它们明确指定了所有可能字段中的字段子集......但底层类型仍然是统一记录类型(类似于JS对象)。

我想我不明白我们为什么这么说:

type alias

而不是

type alias Foo = { bar : Int } 

前者告诉我,type Foo = { bar : Int } 的任何记录都是{ bar : Int },这意味着我Foo{a|bar:Int}属于同一类型a。我错在哪里?我很困惑。我觉得我不喜欢唱歌。

ADDENDUM 2

我的目标不仅仅是让某个字段Foo指定字段上有WithId,而是指定两种类型:.idFoo其中WithId Foo具有结构Foo{ bar:Int }具有结构WithId Foo。我还想拥有{ bar:Int, id:Uuid}WithId Baz等等,WithId Quux使用单个编码器和单个解码器功能。

基本问题是我有持久化和非持久化数据,它们具有完全相同的结构,除了一旦持续存在,我有一个id。并且希望我在某些情况下保留记录的类型级保证,因此WithId a无法正常工作。

3 个答案:

答案 0 :(得分:5)

您对WithId a值的了解以及您应该如何处理它(即将其视为a)的理由在逻辑上是合理的,但Elm对可扩展记录的支持只是不允许使用类型系统。实际上,Elm的可扩展记录类型是专门设计的 not ,以允许访问完整记录。

设计的可扩展记录是声明函数只访问记录的某些字段,然后编译器强制执行该约束:

type alias WithId a =
    { a | id : Uuid }

isValidId : WithId a -> Bool
isValidId withId =
    -- can only access .id

updateId : WithId a -> WithId a
updateId withId =
    { withId |
        id = -- can only access .id
    }

例如,当用于编写在大型程序模型的一小部分上工作的函数时,这为给定函数能够执行的操作提供了强大的保证。例如,您的updateUser函数可以仅限制为访问模型的user字段,而不是通过自由统计来访问模型中的许多字段。当您稍后尝试调试对模型的意外更改时,这些约束可帮助您快速排除许多函数,因为它们负责更改。

对于上述更具体的例子,我推荐Richard Feldman的elm-europe 2017演讲https://youtu.be/DoA4Txr4GUs(该视频中的23:10显示可扩展记录的部分)。

当我第一次学习可扩展记录时,我有同样的兴奋,然后是令人困惑的失望,但事后我认为这来自于我根深蒂固的面向对象编程本能。我看到“可扩展”,我的大脑直接跳到“继承”。但Elm并没有以支持类型继承的方式实现可扩展记录。

如上例所示,可扩展记录的真正目的是将函数的访问限制为只有大记录类型的子集。

至于您的具体用例,我建议您从每个数据类型的Encoder开始,如果您想重用ID编码器的实现,请调用共享{{1每个编码器中的函数。

答案 1 :(得分:2)

我们对WithId a实例的唯一了解是,它有一个.id字段。如果我们对WithId a进行编码,我们可以做的最好的事就是对.id值进行编码。

type alias Uuid =
    String

encodeUuid : Uuid -> Value
encodeUuid =
    Encode.string

type alias WithId a =
    { a | id : Uuid }

encode : WithId a -> Value
encode withId =
    encodeUuid withId.id

答案 2 :(得分:1)

阅读您的附录并重新阅读您的问题,我相信您实际上可以通过Elm中的可扩展记录实现您想要的目标。

这是一个小程序,可以完成你所做的事情并编译​​得很好:

module Main exposing (main)

import Html exposing (Html, pre, text)
import Json.Encode as Json exposing (Value, int, object, string)


main : Html msg
main =
    let
        json =
            Json.encode 2 <|
                encoder recordEncoder record
    in
    pre [] [ text json ]


type alias WithId a =
    { a | id : Int }


type alias Record =
    WithId
        { fieldA : String
        }


record : Record
record =
    { id = 123
    , fieldA = "A"
    }


encoder : (WithId a -> Value) -> WithId a -> Value
encoder recordEncoder value =
    object
        [ ( "id", int value.id )
        , ( "value", recordEncoder value )
        ]


recordEncoder : Record -> Value
recordEncoder record =
    object
        [ ( "fieldA", string record.fieldA ) ]

View on Ellie - The Elm Live Editor

根据我的first answer我仍然非常确定如果你将这种模式扩展到整个程序,它会很好地运作,但是关于Elm的好处是你可以试试东西像这样,如果它没有成功,编译器可以帮助你重构!

...如果你真的需要能够处理你的程序中的持久和非持久记录,你可以在你的类型别名中重复一点:

module Main exposing (main)

import Html exposing (Html, pre, text)
import Json.Encode as Json exposing (Value, int, object, string)


main : Html msg
main =
    let
        json =
            Json.encode 2 <|
                encoder recordEncoder record
    in
    pre [] [ text json ]


type alias WithId a =
    { a
        | id : Int
    }


type alias Record a =
    { a
        | fieldA : String
    }


type alias PersistedRecord =
    { id : Int
    , fieldA : String
    }


record : PersistedRecord
record =
    { id = 123
    , fieldA = "A"
    }


encoder : (WithId a -> Value) -> WithId a -> Value
encoder recordEncoder value =
    object
        [ ( "id", int value.id )
        , ( "value", recordEncoder value )
        ]


recordEncoder : Record a -> Value
recordEncoder record =
    object
        [ ( "fieldA", string record.fieldA ) ]

View on Ellie - The Elm Live Editor

直接回答这部分补遗:

  

我想我不明白我们为什么这么说:

type alias Foo = { bar : Int } 
     

而不是

type Foo = { bar : Int }
     

前者告诉我,任何{ bar : Int }的记录都是Foo,这意味着我{a|bar:Int}a和{{1}的类型相同}。我错在哪里?我很困惑。我觉得我不喜欢唱歌。

Foo不是{ a | bar:Int },因为Foo类型别名指定了一组固定的字段。 Foo包含Foo字段,没有其他字段。如果您想说“带有这些字段的记录,可能还有其他”,则需要使用可扩展记录语法(bar)。

所以我认为你出错的地方在于假设所有记录类型都是可扩展的。他们不是。当函数说它接受或返回带有固定字段集的记录时,这些值将永远不会包含其他字段 - 编译器会保证它。