类型构造函数生成给定类型的类型。例如,Maybe构造函数
data Maybe a = Nothing | Just a
可以是一个具体类型,如Char,并给出一个具体类型,如Maybe Char。在种类方面,有一个
GHCI> :k Maybe
Maybe :: * -> *
我的问题:是否可以定义一个类型构造函数,在给定 Char 的情况下产生具体类型,比如说?换句话说,是否可以在类型构造函数的类型签名中混合种和类型?像
这样的东西GHCI> :k my_type
my_type :: Char -> * -> *
答案 0 :(得分:8)
Haskell类型构造函数是否具有非类型参数?
让我们通过类型参数解压缩你的意思。单词 type 具有(至少)两个潜在含义:你是否意味着类型在狭义的种类*
,或更广泛意义上的类型级别的东西?我们不能(但)使用类型中的值,但现代GHC具有非常丰富的类型语言,允许我们使用除具体类型之外的各种事物作为类型参数。
Haskell中的类型构造函数始终承认非*
参数。例如,仿函数的固定点的编码在普通的旧Haskell 98中工作:
newtype Fix f = Fix { unFix :: f (Fix f) }
ghci> :k Fix
Fix :: (* -> *) -> *
Fix
由类型* -> *
的仿函数参数化,而不是类型*
。
*
和->
DataKinds
扩展程序使用用户声明的类型丰富了GHC的类型系统,因此种类可能由*
和->
之外的其他部分构建。它的工作原理是将所有data
声明提升到种类级别。也就是说,data
声明如
data Nat = Z | S Nat -- natural numbers
引入了种 Nat
和类型构造函数Z :: Nat
和S :: Nat -> Nat
,以及常用的类型和值构造函数。这允许您编写由类型级数据参数化的数据类型,例如惯用的 vector 类型,它是由其长度索引的链接列表。
data Vec n a where
Nil :: Vec Z a
(:>) :: a -> Vec n a -> Vec (S n) a
ghci> :k Vec
Vec :: Nat -> * -> *
有一个名为ConstraintKinds
的相关扩展程序,它可以解除像Ord a
这样的约束,从#34;胖箭头" =>
,允许他们按照自然意图漫游在类型系统的景观中。 Kmett使用这种力量来构建category of constraints,新类型(:-) :: Constraint -> Constraint -> *
表示"蕴含":类型c :- d
的值是c
的证明然后暂挂d
。例如,我们可以证明所有Ord a
Eq [a]
暗示a
:
ordToEqList :: Ord a :- Eq [a]
ordToEqList = Sub Dict
forall
但是,Haskell目前在类型级别和值级别之间保持严格的分离。在程序运行之前,类型级别的事物总是被删除,(几乎)总是可推断的,在表达式中是不可见的,并且(依赖地)由forall
量化。如果您的应用程序需要更灵活的东西,例如对运行时数据的依赖量化,那么您必须使用 singleton 编码手动模拟它。
例如,split
的规范说它根据其(runtime!)参数在一定长度上剪切一个向量。输出向量的类型取决于split
参数的值。我们想写这个...
split :: (n :: Nat) -> Vec (n :+: m) a -> (Vec n a, Vec m a)
...其中我使用类型函数(:+:) :: Nat -> Nat -> Nat
,它代表类型级自然的添加,以确保输入向量至少与n
一样长。 ..
type family n :+: m where
Z :+: m = m
S n :+: m = S (n :+: m)
...但是Haskell不允许宣布split
!没有Z
或S n
类型的任何值;只有类型*
包含值。我们无法在运行时直接访问n
,但我们可以使用我们可以模式匹配的GADT来了解类型级n
是什么:
data Natty n where
Zy :: Natty Z
Sy :: Natty n -> Natty (S n)
ghci> :k Natty
Natty :: Nat -> *
Natty
被称为 singleton ,因为对于给定的(明确定义的)n
,只有一个(明确定义的)类型为Natty n
的值}。我们可以使用Natty n
作为n
的运行时替代。
split :: Natty n -> Vec (n :+: m) a -> (Vec n a, Vec m a)
split Zy xs = (Nil, xs)
split (Sy n) (x :> xs) =
let (ys, zs) = split n xs
in (x :> ys, zs)
无论如何,重点是值 - 运行时数据 - 不能出现在类型中。以单例形式复制Nat
的定义非常繁琐(如果您希望编译器推断出这些值,事情会变得更糟);依赖类型的语言,如Agda,Idris或未来的Haskell,逃避了严格地将类型与值分开的暴政,并为我们提供了一系列富有表现力的量词。您可以使用诚实至善Nat
作为split
的运行时参数,并在返回类型中依赖于其值。
@pigworker已经写了很多关于Haskell在现代依赖类型编程的类型和值之间的严格分离的不适合性。例如,参见the Hasochism paper或他的talk关于未经修饰的假设,这些假设已被四十年的Hindley-Milner风格的编程所鼓舞。
最后,对于它的价值,TypeInType
现代GHC统一了类型和种类,允许我们使用我们用来讨论类型变量的相同工具来讨论类型变量。在关于会话类型的a previous post中,我使用TypeInType
为类型标记的类型级序列定义了一种类型:
infixr 5 :!, :?
data Session = Type :! Session -- Type is a synonym for *
| Type :? Session
| E
答案 1 :(得分:1)
使用广义代数数据类型(GADTS),可以根据输入类型定义具体的输出,例如:
data CustomMaybe a where
MaybeChar :: Maybe a -> CustomMaybe Char
MaybeString :: Maybe a > CustomMaybe String
MaybeBool :: Maybe a -> CustomMaybe Bool
exampleFunction :: CustomMaybe a -> a
exampleFunction (MaybeChar maybe) = 'e'
exampleFunction (MaybeString maybe) = True //Compile error
main = do
print $ exampleFunction (MaybeChar $ Just 10)
为了达到类似的效果,RankNTypes可以允许执行类似的行为:
exampleFunctionOne :: a -> a
exampleFunctionOne el = el
type PolyType = forall a. a -> a
exampleFuntionTwo :: PolyType -> Int
exampleFunctionTwo func = func 20
exampleFunctionTwo func = func "Hello" --Compiler error, PolyType being forced to return 'Int'
main = do
print $ exampleFunctionTwo exampleFunctionOne
PolyType定义允许您在exampleFunctionTwo中插入多态函数并强制其输出为“Int”。
答案 2 :(得分:1)
我建议@Benjamin Hodgson的答案和他给出的参考资料,看看如何使这种事情有用。但是,要更直接地回答您的问题,使用多个扩展程序(DataKinds
,KindSignatures
和GADTs
),您可以定义在(某些)具体类型上参数化的类型。
例如,这里有一个参数化具体Bool
数据类型:
{-# LANGUAGE DataKinds, KindSignatures, GADTs #-}
{-# LANGUAGE FlexibleInstances #-}
module FlaggedType where
-- The single quotes below are optional. They serve to notify
-- GHC that we are using the type-level constructors lifted from
-- data constructors rather than types of the same name (and are
-- only necessary where there's some kind of ambiguity otherwise).
data Flagged :: Bool -> * -> * where
Truish :: a -> Flagged 'True a
Falsish :: a -> Flagged 'False a
-- separate instances, just as if they were different types
-- (which they are)
instance (Show a) => Show (Flagged 'False a) where
show (Falsish x) = show x
instance (Show a) => Show (Flagged 'True a) where
show (Truish x) = show x ++ "*"
-- these lists have types as indicated
x = [Truish 1, Truish 2, Truish 3] -- :: Flagged 'True Integer
y = [Falsish "a", Falsish "b", Falsish "c"] -- :: Flagged 'False String
-- this won't typecheck: it's just like [1,2,"abc"]
z = [Truish 1, Truish 2, Falsish 3] -- won't typecheck
请注意,这与定义两个完全独立的类型没有太大区别:
data FlaggedTrue a = Truish a
data FlaggedFalse a = Falsish a
事实上,我很难想到Flagged
优于定义两种不同类型的任何优势,除非你与某人有赌注,你可以编写有用的Haskell代码而没有类型类。例如,您可以写:
getInt :: Flagged a Int -> Int
getInt (Truish z) = z -- same polymorphic function...
getInt (Falsish z) = z -- ...defined on two separate types
也许其他人可以想到其他一些优势。
无论如何,我相信具有具体值的参数化类型实际上只有在具体类型足够“丰富”时才有用,你可以用它来利用类型检查器,就像本杰明的例子一样。
正如@ user2407038指出的那样,最有趣的原始类型,如Int
s,Char
s,String
等等无法使用此办法。有趣的是,你可以使用文字正整数和字符串作为类型参数,但它们被视为Nat
和Symbol
s(如{{1}中所定义)分别。
所以这样的事情是可能的:
GHC.TypeLits
答案 3 :(得分:0)
没有。 Haskell没有依赖类型(还)。有关何时可以讨论,请参阅https://typesandkinds.wordpress.com/2016/07/24/dependent-types-in-haskell-progress-report/。
与此同时,你可以在Agda,Idris和Cayenne中获得这样的行为。