Reputation:
For a silly challenge I am trying to implement a list type using as little of the prelude as possible and without using any custom types (the data
keyword).
I can construct an modify a list using tuples like so:
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)
I cannot think of how to write an at (!!) function. Is this even possible in a static language? If it is possible could you try to nudge me in the right direction without telling me the answer.
Upvotes: 5
Views: 418
Reputation: 1862
You can implement the datatype List a
as a pair (f, n)
where f :: Nat -> a
and n :: Nat
, where n
is the length of the list:
type List a = (Int -> a, Int)
Implementing the empty list, the list operations cons
, head
, tail
, and null
, and a function convert :: List a -> [a]
is left as an easy exercise.
(Disclaimer: stole this from Bird's Introduction to Functional Programming in Haskell.)
Of course, you could represent tuples via functions as well. And then True
and False
and the natural numbers ...
Upvotes: 3
Reputation: 152737
There is a standard trick known as Church encoding that makes this easy. Here's a generic example to get you started:
data Foo = A Int Bool | B String
fooValue1 = A 3 False
fooValue2 = B "hello!"
Now, a function that wants to use this piece of data must know what to do with each of the constructors. So, assuming it wants to produce some result of type r
, it must at the very least have two functions, one of type Int -> Bool -> r
(to handle the A
constructor), and the other of type String -> r
(to handle the B
constructor). In fact, we could write the type that way instead:
type Foo r = (Int -> Bool -> r) -> (String -> r) -> r
You should read the type Foo r
here as saying "a function that consumes a Foo
and produces an r
". The type itself "stores" a Foo
inside a closure -- so that it will effectively apply one or the other of its arguments to the value it closed over. Using this idea, we can rewrite fooValue1
and fooValue2
:
fooValue1 = \consumeA consumeB -> consumeA 3 False
fooValue2 = \consumeA consumeB -> consumeB "hello!"
Now, let's try applying this trick to real lists (though not using Haskell's fancy syntax sugar).
data List a = Nil | Cons a (List a)
Following the same format as before, consuming a list like this involves either giving a value of type r
(in case the constructor was Nil
) or telling what to do with an a
and another List a
, so. At first, this seems problematic, since:
type List a r = (r) -> (a -> List a -> r) -> r
isn't really a good type
(it's recursive!). But we can instead demand that we first reduce all the recursive arguments to r
first... then we can adjust this type to make something more reasonable.
type List a r = (r) -> (a -> r -> r) -> r
(Again, we should read the type List a r
as being "a thing that consumes a list of a
s and produces an r
".)
There's one final trick that's necessary. What we would like to do is to enforce the requirement that the r
that our List a r
returns is actually constructed from the arguments we pass. That's a little abstract, so let's give an example of a bad value that happens to have type List a r
, but which we'd like to rule out.
badList = \consumeNil consumeCons -> False
Now, badList
has type List a Bool
, but it's not really a function that consumes a list and produces a Bool
, since in some sense there's no list being consumed. We can rule this out by demanding that the type work for any r
, no matter what the user wants r
to be:
type List a = forall r. (r) -> (a -> r -> r) -> r
This enforces the idea that the only way to get an r
that gets us off the ground is to use the (user-supplied) consumeNil
function. Can you see how to make this same refinement for our original Foo
type?
Upvotes: 12
Reputation: 36622
Let's set aside at
, and just think about your first four functions for the moment. You haven't given them type signatures, so let's look at those; they'll make things much clearer. The types are
cons :: a -> (a, ())
prepend :: a -> b -> (a, b)
head :: (a, b) -> a
tail :: (a, b) -> b
Hmmm. Compare these to the types of the corresponding Prelude functions1:
return :: a -> [a]
(:) :: a -> [a] -> [a]
head :: [a] -> a
tail :: [a] -> [a]
The big difference is that, in your code, there's nothing that corresponds to the list type, []
. What would such a type be? Well, let's compare, function by function.
cons
/return
: here, (a,())
corresponds to [a]
prepend
/(:)
: here, both b
and (a,b)
correspond to [a]
head
: here, (a,b)
corresponds to [a]
tail
: here, (a,b)
corresponds to [a]
It's clear, then, that what you're trying to say is that a list is a pair. And prepend
indicates that you then expect the tail of the list to be another list. So what would that make the list type? You'd want to write type List a = (a,List a)
(although this would leave out ()
, your empty list, but I'll get to that later), but you can't do this—type synonyms can't be recursive. After all, think about what the type of at
/!!
would be. In the prelude, you have (!!) :: [a] -> Int -> a
. Here, you might try at :: (a,b) -> Int -> a
, but this won't work; you have no way to convert a b
into an a
. So you really ought to have at :: (a,(a,b)) -> Int -> a
, but of course this won't work either. You'll never be able to work with the structure of the list (neatly), because you'd need an infinite type. Now, you might argue that your type does stop, because ()
will finish a list. But then you run into a related problem: now, a length-zero list has type ()
, a length-one list has type (a,())
, a length-two list has type (a,(a,()))
, etc. This is the problem: there is no single "list type" in your implementation, and so at
can't have a well-typed first parameter.
You have hit on something, though; consider the definition of lists:
data List a = []
| a : [a]
Here, [] :: [a]
, and (:) :: a -> [a] -> [a]
. In other words, a list is isomorphic to something which is either a singleton value, or a pair of a value and a list:
newtype List' a = List' (Either () (a,List' a))
You were trying to use the same trick without creating a type, but it's this creation of a new type which allows you to get the recursion. And it's exactly your missing recursion which allows lists to have a single type.
1: On a related note, cons
should be called something like singleton
, and prepend
should be cons
, but that's not important right now.
Upvotes: 3
Reputation: 77384
If it is possible could you try and nudge me in the right direction without telling me the answer.
It's possible, in more than one way. But your main problem here is that you've not implemented lists. You've implemented fixed-size vectors whose length is encoded in the type.
Compare the types from adding an element to the head of a list vs. your implementation:
(:) :: a -> [a] -> [a]
prepend :: a -> b -> (a, b)
To construct an equivalent of the built-in list type, you'd need a function like prepend
with a type resembling a -> b -> b
. And if you want your lists to be parameterized by element type in a straightforward way, you need the type to further resemble a -> f a -> f a
.
Is this even possible in a static language?
You're also on to something here, in that the encoding you're using works fine in something like Scheme. Languages with "dynamic" systems can be regarded as having a single static type with implicit conversions and metadata attached, which obviously solves the type mismatch problem in a very extreme way!
I cannot think of how to write an at (!!) function.
Recalling that your "lists" actually encode their length in their type, it should be easy to see why it's difficult to write functions that do anything other than increment/decrement the length. You can actually do this, but it requires elaborate encoding and more advanced type system features. A hint in this direction is that you'll need to use type-level numbers as well. You'd probably enjoy doing this as an exercise as well, but it's much more advanced than encoding lists.
Upvotes: 4
Reputation: 25654
Solution A - nested tuples:
Your lists are really nested tuples - for example, they can hold items of different types, and their type reveals their length.
It is possible to write indexing-like function for nested tuples, but it is ugly, and it won't correspond to Prelude's lists. Something like this:
class List a b where ...
instance List () b where ...
instance List a b => List (b,a) b where ...
Solution B - use data
I recommend using data
construct. Tuples are internally something like this:
data (,) a b = Pair a b
so you aren't avoiding data
. The division between "custom types" and "primitive types" is rather artificial in Haskell, as opposed to C.
Solution C - use newtype:
If you are fine with newtype
but not data
:
newtype List a = List (Maybe (a, List a))
Solution D - rank-2-types:
Use 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]
and write functions for them. I think this is closest to your goal. Google for "Church encoding" if you need more hints.
Upvotes: 3