Haskell类型构造函数可以具有非类型参数吗?

时间:2016-12-22 20:24:48

标签: haskell types

类型构造函数生成给定类型的类型。例如,Maybe构造函数

data Maybe a = Nothing | Just a

可以是一个具体类型,如Char,并给出一个具体类型,如Maybe Char。在种类方面,有一个

GHCI> :k Maybe
Maybe :: * -> *

我的问题:是否可以定义一个类型构造函数,在给定 Char 的情况下产生具体类型,比如说?换句话说,是否可以在类型构造函数的类型签名中混合类型?像

这样的东西
GHCI> :k my_type
my_type :: Char -> * -> *

4 个答案:

答案 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 :: NatS :: 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!没有ZS 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的答案和他给出的参考资料,看看如何使这种事情有用。但是,要更直接地回答您的问题,使用多个扩展程序(DataKindsKindSignaturesGADTs),您可以定义在(某些)具体类型上参数化的类型。

例如,这里有一个参数化具体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等等无法使用此办法。有趣的是,你可以使用文字正整数和字符串作为类型参数,但它们被视为NatSymbol s(如{{1}中所定义)分别。

所以这样的事情是可能的:

GHC.TypeLits

答案 3 :(得分:0)

没有。 Haskell没有依赖类型(还)。有关何时可以讨论,请参阅https://typesandkinds.wordpress.com/2016/07/24/dependent-types-in-haskell-progress-report/

与此同时,你可以在Agda,Idris和Cayenne中获得这样的行为。