使用函数创建列表类型

时间:2011-07-15 15:20:57

标签: haskell

对于一个愚蠢的挑战,我尝试使用尽可能少的前奏来实现列表类型,而不使用任何自定义类型(data关键字)。

我可以使用像这样的元组构建一个修改列表:

import Prelude (Int(..), Num(..), Eq(..))

cons x = (x, ())
prepend x xs = (x, xs) 
head (x, _) = x
tail (_, x) = x

at xs n = if n == 0 then xs else at (tail xs) (n-1)

我想不出怎么写at(!!)函数。这甚至可以用静态语言吗? 如果可能的话,你可以尝试在没有告诉我答案的情况下朝正确的方向推动我。

5 个答案:

答案 0 :(得分:12)

有一种称为Church encoding的标准技巧使这很容易。这是一个让您入门的通用示例:

data Foo = A Int Bool | B String
fooValue1 = A 3 False
fooValue2 = B "hello!"

现在,想要使用这段数据的函数必须知道如何处理每个构造函数。因此,假设它想要生成类型r的某些结果,它必须至少具有两个函数,一个类型Int -> Bool -> r(用于处理A构造函数),另一个函数类型String -> r(用于处理B构造函数)。事实上,我们可以用这种方式编写类型:

type Foo r = (Int -> Bool -> r) -> (String -> r) -> r

您应该在此处阅读Foo r类型,说明“消耗Foo并生成r”的函数。类型本身在封闭内“存储”Foo - 以便它有效地将其中一个或另一个参数应用于它关闭的值。使用这个想法,我们可以重写fooValue1fooValue2

fooValue1 = \consumeA consumeB -> consumeA 3 False
fooValue2 = \consumeA consumeB -> consumeB "hello!"

现在,让我们尝试将此技巧应用于真实列表(虽然不使用Haskell的花哨语法糖)。

data List a = Nil | Cons a (List a)

遵循与以前相同的格式,使用这样的列表要么提供类型r的值(如果构造函数是Nil),要么告诉如何处理{{1}和另一个a,所以。起初,这似乎有问题,因为:

List a

并不是一个很好的type List a r = (r) -> (a -> List a -> r) -> r (它是递归的!)。但我们可以要求首先将所有递归参数减少到type ...然后我们可以调整此类型以使某些更合理。

r

(同样,我们应该将类型type List a r = (r) -> (a -> r -> r) -> r 读作“消耗List a r列表并生成a”的内容。)

有一个必要的最后一招。我们想要做的是强制要求我们r返回的r 实际从我们传递的参数构造。这有点抽象,所以让我们给出一个错误值的例子,它碰巧有List a r类型,但我们想排除它。

List a r

现在,badList = \consumeNil consumeCons -> False 的类型为badList,但它不是真正一个消耗列表并产生List a Bool的函数,因为在某种意义上说没有正在消费的名单。我们可以通过要求任何 Bool的类型工作来解决这个问题,无论用户想要r是什么:

r

这强化了这样一种想法,即获得type List a = forall r. (r) -> (a -> r -> r) -> r 让我们离开地面的唯一方法是使用(用户提供的)r函数。你能看到如何对我们原来的consumeNil类型进行同样的改进吗?

答案 1 :(得分:4)

  

如果可能的话,你可以尝试在没有告诉我答案的情况下朝正确的方向推动我。

这可能不止一种方式。但是你的主要问题是你没有实现列表。您已经实现了固定大小的向量,其长度以类型编码。

比较从添加元素到列表头部的类型与实现的对比:

(:) :: a -> [a] -> [a]
prepend :: a -> b -> (a, b)

要构建内置列表类型的等效项,您需要一个类似prepend类型的a -> b -> b函数。如果您希望以简单的方式按元素类型对列表进行参数化,则需要使用类型进一步类似a -> f a -> f a

  

这甚至可以用静态语言吗?

你也在这里做点什么,因为你使用的编码在Scheme之类的工作中运行良好。具有“动态”系统的语言可以被视为具有隐式转换和附加元数据的单个静态类型,这显然以非常极端的方式解决了类型不匹配问题!

  

我想不出如何编写at(!!)函数。

回想一下你的“列表”实际上在它们的类型中编码它们的长度,应该很容易理解为什么编写除递增/递减长度之外的任何事情的函数都很困难。您实际上可以这样做,但它需要精心编码和更高级的类型系统功能。这个方向的提示是你也需要使用类型级数字。您可能也喜欢将此作为练习,但它比编码列表更先进。

答案 2 :(得分:3)

解决方案A - 嵌套元组:

你的列表实际上是嵌套的元组 - 例如,它们可以容纳不同类型的项目,它们的类型显示它们的长度。

可以为嵌套元组编写类似索引的函数,但它很难看,并且它不会与Prelude的列表相对应。像这样:

 class List a b where ...
 instance List () b where ...
 instance List a b => List (b,a) b where ...

解决方案B - 使用数据

我建议使用data构造。元组在内部是这样的:

 data (,) a b = Pair a b

所以你没有避免data。 “自定义类型”和“原始类型”之间的划分在Haskell中是相当人为的,而不是C。

解决方案C - 使用newtype:

如果你对newtype但不是data

没事
 newtype List a = List (Maybe (a, List a))

解决方案D - rank-2-types:

使用rank-2-types:

 type List a = forall b. b -> (a -> b -> b) -> b

 list :: List Int
 list = \n c -> c 1 (c 2 n)   -- [1,2]

并为它们编写函数。我认为这最接近你的目标。如果您需要更多提示,Google可以使用“教会编码”。

答案 3 :(得分:3)

让我们留出at,暂时考虑一下你的前四个功能。你没有给他们签名类型,所以让我们看看那些;他们会让事情变得更加清晰。类型是

cons    :: a -> (a, ())
prepend :: a -> b -> (a, b)
head    :: (a, b) -> a
tail    :: (a, b) -> b

嗯。将它们与相应的Prelude函数 1

的类型进行比较
return :: a -> [a]
(:)    :: a -> [a] -> [a]
head   :: [a] -> a
tail   :: [a] -> [a]

最大的区别在于,在您的代码中,没有任何内容与列表类型[]相对应。这种类型会是什么?好吧,让我们比较,按功能进行比较。

  • cons / return:此处,(a,())对应[a]
  • prepend / (:):此处,b(a,b)都对应[a]
  • head:此处(a,b)对应[a]
  • tail:此处(a,b)对应[a]

很明显,你要说的是列表是一对。并且prepend表示您希望列表的尾部是另一个列表。那么列表类型会是什么?你想写type List a = (a,List a)(虽然这会遗漏(),你的空列表,但我会稍后再说),但是你不能这样做类型的同义词不能是递归的。毕竟,请考虑at / !!的类型。在前奏中,您有(!!) :: [a] -> Int -> a。在这里,您可以尝试at :: (a,b) -> Int -> a,但这不起作用;您无法将b转换为a。所以你真的应该有at :: (a,(a,b)) -> Int -> a,但当然这也行不通。你将永远无法使用列表的结构(整齐地),因为你需要一个无限类型。现在,你可能会认为你的 类型会停止,因为()会完成一个列表。但是你遇到了一个相关的问题:现在,长度为零的列表的类型为(),长度为一的列表的类型为(a,()),长度为二的列表的类型为(a,(a,())),这就是问题: 在您的实现中没有单一的“列表类型”,因此at不能有良好类型的第一个参数。

但是,你已经找到了一些东西;考虑列表的定义:

data List a = []
            | a : [a]

此处[] :: [a](:) :: a -> [a] -> [a]。换句话说,列表是同形的,可以是单例值,也可以是一对值和列表:

newtype List' a = List' (Either () (a,List' a))

你试图在不创建类型的情况下使用相同的技巧,但是这是一种新类型的创建,它允许你获得递归。而且这正是您丢失的递归,它允许列表具有单一类型。


1:在相关提示中,cons应该被称为singleton,而prepend应该是cons,但那不是现在很重要。

答案 4 :(得分:3)

您可以将List a数据类型(f, n)配对为f :: Nat -> an :: Nat,其中n是列表的长度:

type List a = (Int -> a, Int)

实现空列表,列表操作consheadtailnull以及函数convert :: List a -> [a]仍然是一个简单的练习

(免责声明:偷走了Bird的 Haskell中的函数式编程简介。)

当然,您也可以通过函数表示元组。然后是TrueFalse以及自然数字......