【发布时间】:2018-01-12 05:23:03
【问题描述】:
我目前正在阅读Type-Driven Development with Idris 的书。
关于第 6 章中示例数据存储的设计,我有两个问题。数据存储是一个命令行应用程序,允许用户设置其中存储的数据类型,然后添加新数据。
以下是代码的相关部分(略作简化)。你可以在 Github 上看到full code:
module Main
import Data.Vect
infixr 4 .+.
-- This datatype is to define what sorts of data can be contained in the data store.
data Schema
= SString
| SInt
| (.+.) Schema Schema
-- This is a type-level function that translates a Schema to an actual type.
SchemaType : Schema -> Type
SchemaType SString = String
SchemaType SInt = Int
SchemaType (x .+. y) = (SchemaType x, SchemaType y)
-- This is the data store. It can potentially be storing any sort of schema.
record DataStore where
constructor MkData
schema : Schema
size : Nat
items : Vect size (SchemaType schema)
-- This adds new data to the datastore, making sure that the new data is
-- the same type that the DataStore holds.
addToStore
: (dataStore : DataStore) -> SchemaType (schema dataStore) -> DataStore
addToStore (MkData schema' size' store') newitem =
MkData schema' _ (addToData store')
where
addToData
: Vect size' (SchemaType schema') -> Vect (size' + 1) (SchemaType schema')
addToData xs = xs ++ [newitem]
-- These are commands the user can use on the command line. They are able
-- to change the schema, and add new data.
data Command : Schema -> Type where
SetSchema : (newSchema : Schema) -> Command schema
Add : SchemaType schema -> Command schema
-- Given a Schema, this parses input from the user into a Command.
parse : (schema : Schema) -> String -> Maybe (Command schema)
-- This is the main workhorse of the application. It parses user
-- input, turns it into a command, then evaluates the command and
-- returns an updated DataStore.
processInput
: (dataStore : DataStore) -> String -> Maybe (String, DataStore)
processInput dataStore@(MkData schema' size' items') input =
case parse schema' input of
Nothing => Just ("Invalid command\n", dataStore)
Just (SetSchema newSchema) =>
Just ("updated schema and reset datastore\n", MkData newSchema _ [])
Just (Add item) =>
let newStore = addToStore (MkData schema' size' items') item
in Just ("ID " ++ show (size dataStore) ++ "\n", newStore)
-- This kicks off processInput with an empty datastore and a simple Schema
-- of SString.
main : IO ()
main = replWith (MkData SString _ []) "Command: " processInput
这很有意义且易于使用,但有一点让我对设计感到烦恼。 为什么DataStore 包含Schema 而不是被一个索引?
因为DataStore 没有被Schema 索引,我们可能编写了一个错误的addToStore 函数,如下所示:
addToStore
: (dataStore : DataStore) -> SchemaType (schema dataStore) -> DataStore
addToStore _ newitem =
MkData SInt _ []
这是我想象的更多类型安全代码的样子。你可以在 Github 上看到full code:
module Main
import Data.Vect
infixr 4 .+.
data Schema
= SString
| SInt
| (.+.) Schema Schema
SchemaType : Schema -> Type
SchemaType SString = String
SchemaType SInt = Int
SchemaType (x .+. y) = (SchemaType x, SchemaType y)
record DataStore (schema : Schema) where
constructor MkData
size : Nat
items : Vect size (SchemaType schema)
addToStore
: (dataStore : DataStore schema) ->
SchemaType schema ->
DataStore schema
addToStore {schema} (MkData size' store') newitem =
MkData _ (addToData store')
where
addToData
: Vect size' (SchemaType schema) -> Vect (size' + 1) (SchemaType schema)
addToData xs = xs ++ [newitem]
data Command : Schema -> Type where
SetSchema : (newSchema : Schema) -> Command schema
Add : SchemaType schema -> Command schema
parse : (schema : Schema) -> String -> Maybe (Command schema)
processInput
: (schema : Schema ** DataStore schema) ->
String ->
Maybe (String, (newschema ** DataStore newschema))
processInput (schema ** (MkData size' items')) input =
case parse schema input of
Nothing => Just ("Invalid command\n", (_ ** MkData size' items'))
Just (SetSchema newSchema) =>
Just ("updated schema and reset datastore\n", (newSchema ** MkData _ []))
Just (Add item) =>
let newStore = addToStore (MkData size' items') item
msg = "ID " ++ show (size newStore) ++ "\n"
in Just (msg, (schema ** newStore))
main : IO ()
main = replWith (SString ** MkData _ []) "Command: " processInput
这是我的两个问题:
为什么这本书没有使用
DataStore类型的更安全的版本(Schema索引的那个)?使用不太安全的版本(只包含Schema)有什么好处吗?-
被另一种类型索引的类型与包含另一种类型的理论区别是什么?这种差异会根据您使用的语言而改变吗?
例如,我想 Idris 可能没有太大的不同,但 Haskell 有很大的不同。 (对吧……?)
我刚开始玩 Idris(而且我对在 Haskell 中使用单例或 GADT 不是特别精通),所以我很难理解这一点。如果您能指出任何有关这方面的论文,我会非常感兴趣。
【问题讨论】:
-
@Shersh 和 OP:作者实际上在本书后面的部分中进行了这种转换(参见第 10.3.2 节)。这是code from the book
-
@AntonTrunov 这证明了这种转换更好。也许第一个是为了简洁而选择的。
-
@Shersh 嗯,我认为这主要是品味问题。我个人更喜欢一个更简单的数据类型,它有几个关于它的使用的引理。通过这种方式,您可以编写代码并稍后证明它的一些属性。就像你可以使用列表,编写你的程序 ML-(或 Haskell-)风格,然后证明它们,或者你可以使用像向量这样臭名昭著的数据类型,在这种情况下,你有时甚至不能声明它的值的属性(我的意思是不使用异质平等,又名约翰少校平等)。见,例如this answer.
-
@AntonTrunov 这本书确实在本书后面使用了转换,但它指出,“不是将模式存储为记录中的字段,而是通过数据模式参数化记录,因为你不'不打算让架构被更新。” (第 10.3.2 节)我不明白这个评论。在我上面发布的代码中,存储由模式参数化,但仍然允许通过使用依赖对来更改。
-
@illabout 该评论意味着例如
addToStore无法更改输出数据存储的架构。要更改架构,您需要使用一些 external 机制,例如依赖对,这使您打算明确更改架构,而以前版本的代码并非如此。
标签: types records dependent-type idris phantom-types