可扩展记录和高阶函数
Extensible Records and Higher Order Functions
我想为具有 id
字段且类型为 Uuid
的记录指定类型。所以,我正在使用可扩展记录。类型如下所示:
type alias WithId a = { a | id : Uuid }
到目前为止,还不错。但是后来我为这种类型创建了一个 Json.Encoder
—— 即类型为 WithId a -> Value
的函数。我需要一种对基础值进行编码的方法,所以我想采用 a -> Value
类型的第一个参数。因为我知道 a
是一个记录,所以我可以安全地假设它被编码为一个 JSON 对象,即使 Elm 没有公开 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
不应该通过 type alias
结构类型成为 a
?
我错过了什么?为什么 WithId a
的类型别名不允许我使用 WithId a
作为 a
的实例,即使它被定义为 {a|...}
?
附录
我已经在下面标记了一个答案(相当于"You just can't do that, but here's what you're supposed to be doing."),但我还是有点不满意。我想我对术语 type alias
的使用感到困惑。我的理解是记录总是 type alias
,因为它们明确指定了所有可能字段中的字段子集……但底层类型仍然是统一的记录类型(类似于 JS 对象)。
我想我不明白为什么我们说:
type alias Foo = { bar : Int }
而不是
type Foo = { bar : Int }
前者对我来说意味着任何带有 { bar : Int }
的记录都是 Foo
,这对我来说意味着 {a|bar:Int}
与 a
的类型相同和 Foo
。我哪里错了?我很困惑。我觉得我不是在摸索记录。
附录 2
我的目标不仅仅是拥有一个类型 WithId
来指定字段上有一个 .id
,而是拥有两种类型:Foo
和 WithId Foo
其中 Foo
具有结构 { bar:Int }
,WithId Foo
具有结构 { bar:Int, id:Uuid}
。然后,我还想有一个 WithId Baz
、WithId Quux
等,具有用于 WithId a
.
的单个编码器和单个解码器功能
基本问题是我有持久化和非持久化数据,它们具有完全相同的结构,除了一旦持久化,我就有一个id。并且希望我类型级别保证记录在某些情况下保持不变,因此 Maybe
不起作用。
我们对 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
您关于您对 WithId a
值的了解以及您应该能够用它做什么(即将其视为 a
)的推理在逻辑上是合理的,但 Elm 支持可扩展记录只是不以允许的方式与类型系统一起玩。事实上,Elm 的可扩展记录类型是专门设计的 而不是 以允许访问完整记录。
可扩展记录的设计目的是声明一个函数将只访问记录的某些字段,然后编译器强制执行该约束:
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 编码器的实现,调用共享 idEncoder
在每个编码器中运行。
阅读您的附录并重新阅读您的问题,我相信您实际上可以通过 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
根据我的 我仍然很确定如果将此模式扩展到整个程序,它不会很好地工作,但 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
直接回答您的这部分附录:
I guess I don't understand why we say:
type alias Foo = { bar : Int }
instead of
type Foo = { bar : Int }
The former implies to me that any record with { bar : Int }
is a Foo, which would imply to me that {a|bar:Int}
is both of the same type as a
and a Foo
. Where am I wrong here? I'm confused. I feel like I'm not grokking records.
{ a | bar:Int }
不是 Foo
,因为 Foo
类型别名指定了一组固定的字段。 Foo
包含一个 bar
字段 而没有其他字段 。如果您想说“包含这些字段 和其他字段 的记录”,您需要使用可扩展记录语法 ({ a | …fields… }
).
所以我认为您出错的地方在于假设所有记录类型都是可扩展的。他们不是。当一个函数说它接受或 returns 具有一组固定字段的记录时,这些值将永远不会包含其他字段——编译器保证这一点。
我想为具有 id
字段且类型为 Uuid
的记录指定类型。所以,我正在使用可扩展记录。类型如下所示:
type alias WithId a = { a | id : Uuid }
到目前为止,还不错。但是后来我为这种类型创建了一个 Json.Encoder
—— 即类型为 WithId a -> Value
的函数。我需要一种对基础值进行编码的方法,所以我想采用 a -> Value
类型的第一个参数。因为我知道 a
是一个记录,所以我可以安全地假设它被编码为一个 JSON 对象,即使 Elm 没有公开 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
不应该通过 type alias
结构类型成为 a
?
我错过了什么?为什么 WithId a
的类型别名不允许我使用 WithId a
作为 a
的实例,即使它被定义为 {a|...}
?
附录
我已经在下面标记了一个答案(相当于"You just can't do that, but here's what you're supposed to be doing."),但我还是有点不满意。我想我对术语 type alias
的使用感到困惑。我的理解是记录总是 type alias
,因为它们明确指定了所有可能字段中的字段子集……但底层类型仍然是统一的记录类型(类似于 JS 对象)。
我想我不明白为什么我们说:
type alias Foo = { bar : Int }
而不是
type Foo = { bar : Int }
前者对我来说意味着任何带有 { bar : Int }
的记录都是 Foo
,这对我来说意味着 {a|bar:Int}
与 a
的类型相同和 Foo
。我哪里错了?我很困惑。我觉得我不是在摸索记录。
附录 2
我的目标不仅仅是拥有一个类型 WithId
来指定字段上有一个 .id
,而是拥有两种类型:Foo
和 WithId Foo
其中 Foo
具有结构 { bar:Int }
,WithId Foo
具有结构 { bar:Int, id:Uuid}
。然后,我还想有一个 WithId Baz
、WithId Quux
等,具有用于 WithId a
.
基本问题是我有持久化和非持久化数据,它们具有完全相同的结构,除了一旦持久化,我就有一个id。并且希望我类型级别保证记录在某些情况下保持不变,因此 Maybe
不起作用。
我们对 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
您关于您对 WithId a
值的了解以及您应该能够用它做什么(即将其视为 a
)的推理在逻辑上是合理的,但 Elm 支持可扩展记录只是不以允许的方式与类型系统一起玩。事实上,Elm 的可扩展记录类型是专门设计的 而不是 以允许访问完整记录。
可扩展记录的设计目的是声明一个函数将只访问记录的某些字段,然后编译器强制执行该约束:
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 编码器的实现,调用共享 idEncoder
在每个编码器中运行。
阅读您的附录并重新阅读您的问题,我相信您实际上可以通过 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
根据我的
…如果你真的需要在你的程序中同时处理持久化和非持久化记录,你只需在你的类型别名中重复一点就可以做到:
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
直接回答您的这部分附录:
I guess I don't understand why we say:
type alias Foo = { bar : Int }
instead of
type Foo = { bar : Int }
The former implies to me that any record with
{ bar : Int }
is a Foo, which would imply to me that{a|bar:Int}
is both of the same type asa
and aFoo
. Where am I wrong here? I'm confused. I feel like I'm not grokking records.
{ a | bar:Int }
不是 Foo
,因为 Foo
类型别名指定了一组固定的字段。 Foo
包含一个 bar
字段 而没有其他字段 。如果您想说“包含这些字段 和其他字段 的记录”,您需要使用可扩展记录语法 ({ a | …fields… }
).
所以我认为您出错的地方在于假设所有记录类型都是可扩展的。他们不是。当一个函数说它接受或 returns 具有一组固定字段的记录时,这些值将永远不会包含其他字段——编译器保证这一点。