Skip to content

Instantly share code, notes, and snippets.

@rehno-lindeque
Last active May 25, 2016 19:14
Show Gist options
  • Save rehno-lindeque/0fd273eaeab2ab2dc48c to your computer and use it in GitHub Desktop.
Save rehno-lindeque/0fd273eaeab2ab2dc48c to your computer and use it in GitHub Desktop.
Elm tuple proposal

Overview

Lately I've been annoyed that Haskell has tuples with more than 2 elements. It seems to me like a great deal of boilerplate in Haskell comes from writing code like Data.Profunctor.Product. One can find many, many more examples like this in Haskell libraries.

That is to say, suppose you could define types like this:

type alias A = (x,(x,x))

or:

type alias B = ((x,x),x)

but this were banned:

type alias Triple = (x,x,x)

would you lose anything important? After all, you could still do this manually instead:

type Triple' = Triple' x x x

This would free up (a,b,c) so that it could be used as syntax sugar instead:

(1,(2,3)) == (1,2,3)

Is this an idea that is worth exploring?

Possible objections

I haven't found the objections to this in say Why is (a,b,c,d) not sugar for (a,(b,(c,(d,()))))? and N-ary tuples vs pairs very compelling. The answer in the second link claims that it would weaken the type system. However, as I've demonstrated above, a manually defined type Triple' would easily recover anything that was lost. Furthermore, I'm not advocating any changes to the type system. The following inequality would remain:

(1,(2,3)) /= ((1,2),3)

so that

(1,2,3) /= ((1,2),3)

Instead, this would be a simplification since the equality (1,2,3) == (1,(2,3)) is enforced as syntax sugar rather than a type system extension.

Additional comments

Chaining binary functions that act on or construct tuples

This would make it possible to write much simpler code if Elm happened to get say bifunctors or arrows using an operator similar to (***):

type MyRecord =
  { a : String
  , b : Int
  , c : Int
  , d : [Int]
  , e : String
  }

let myTuple = ("Hello", "4", 8, [10.1, 9.3, 4.5], 'C')
in uncurry5 MyRecord 
     <| (identity *** fromString *** identity *** map ceiling *** toString)
     <| myTuple

...in the future. Unfortunately infix style functions bracket in the wrong direction though, so that bimap does not work...

type MyRecord =
  { a : String
  , b : Int
  , c : Int
  , d : [Int]
  , e : String
  }

let myTuple = ("Hello", "4", 8, [10.1, 9.3, 4.5], 'C')
in uncurry5 MyRecord 
     <| identity 
        `bimap` fromString
        `bimap` identity
        `bimap` map ceiling
        `bimap` toString
     <| myTuple
     -- Type error: (a,(b,(c,(d,e)))) does not match ((((a,b),c),d),e)

...never-the-less, I think at some point this sort of code will become desirable.

Generic programming

Also consider that something nearly recursive is now possible when it comes to defining functions like uncurry*:

uncurry3 : (a -> b -> c -> d) -> (a,(b,c)) -> d
uncurry3 f (a,bc) = uncurry (f a) bc

uncurry4 : (a -> b -> c -> d -> e) -> (a,(b,(c,d))) -> e
uncurry4 f (a,bcd) = uncurry3 (f a) bcd

uncurry5 : (a -> b -> c -> d -> e -> f) -> (a,(b,(c,(d,e)))) -> f
uncurry5 f (a,bcde) = uncurry4 (f a) bcde

or with the syntax sugar applied to the types:

uncurry3 : (a -> b -> c -> d) -> (a,b,c) -> d
uncurry3 f (a,bc) = uncurry (f a) bc

uncurry4 : (a -> b -> c -> d -> e) -> (a,b,c,d) -> e
uncurry4 f (a,bcd) = uncurry3 (f a) bcd

uncurry5 : (a -> b -> c -> d -> e -> f) -> (a,b,c,d,e) -> f
uncurry5 f (a,bcde) = uncurry4 (f a) bcde

I could imagine some future generic programming mechanism being significantly simpler as a result.

@TheSeamau5
Copy link

Like: (a,b) doesn't tell me its size. It just tells me that the first element is (potentially) of a different type from the second element but the second element could itself hide more stuff.

Oh, so on this... it's sort of already true anyway. I mean if you have a function

Well, there's a case I was thinking about from a beginner's perspective. See the thing about fst suddenly working on tuples of all sizes. snd does not anymore (again, see this from a beginner's perspective).

Say we have this:

type alias Vector3 = (Float, Float, Float)

Obviously, this is just sugar for:

type alias Vector3 = (Float, (Float, Float))

Now, say you have a vector v = (1,2,3), if you say fst v you get 1 but snd v will get you (2,3).

This is to be contrasted with if you have the following type:

type alias Vector2 = (Float, Float)

If v = (1,2) then fst v is 1 and snd v is 2. This inconsistent behavior of snd can throw people off and then you have to say: "oh, you know, a 3-tuple is really a 2-tuple inside another 2-tuple"... and I'm worried that gets weird early on.

Take this example with a grain of salt though because obviously you'd want to represent vectors as a record with x, y, z fields.

But I think it's a concern from a beginner's perspective. On the other hand, today you can just say fst and snd only work on 2-tuples. Period. Any higher use pattern matching.

But then again, I'm biased. I don't like tuples when records exist. I mostly use them either because I'm using an API that spits out tuples like Window.dimensions or to use the empty tuple for laziness or void.

@rehno-lindeque
Copy link
Author

Well, there's a case I was thinking about from a beginner's perspective. See the thing about fst suddenly working on tuples of all sizes. snd does not anymore (again, see this from a beginner's perspective).

Oh right, I agree. I think the semantics of this does seem a bit cloudy if snd remains and produces (Float,Float) from (Float,Float,Float). This is a weakness in the proposal. With the syntax sugar for n-tuples included I don't see any way around it except to either do away with just snd, or rename it, or rename both fst and snd... Perhaps someone will chime in with an idea.

@rehno-lindeque
Copy link
Author

On mathematical aggregates like vectors etc

This is a bit of a side note, but a lot of people are mentioning math vectors for tuples versus records. I want to play devil's advocate a little bit because I must admit for 3D vectors, 4x4 matrices etc when the math gets hairy I often prefer having numeric indexes around for indexing those object.

Indexing into math objects

E.g. using Array, so that I can write the code:

vectorAlongAxis : Int -> List Int
vectorAlongAxis axisIndex = Array.set axisIndex 1.0 (Array.fromList [0.0,0.0,0.0])

...that kind of code can get quite annoying if you have to write case axisIndex of to do the math.

Data packing / performance etc

Also, I wonder if records would have an efficient enough representation to use for 3D graphics. It seems to me that if you construct a large array of 3D vectors they should be ideally be compiled into something like TypedArray or similar.

Anyway, I'm not sure what the underlying representation for { x: 1, y: 0, z: 0 } looks like versus [1,0,0] in JavaScript, or even what (1,0,0) looks like for Elm?

To make it more concrete, how should one represent a 4x4 matrix efficiently? I imagine a tuple is probably as efficient as it gets?
E.g. that

((1.0, 0.0, 0.0, 0.0), (0.0, 1.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, 0.0, 0.0, 1.0))

...could (in future) compile down to

[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]

...in javascript ideally.

Constructing new, larger aggregate objects

It's also often convenient to construct matrices out of basis vectors for example, which is more cumbersome with records. E.g.

xAxis = (1,0,0)
yAxis = (0,1,0)
zAxis = (0,0,1)
myTransform = {- some magical projection perhaps -}
matrix3x3 =
  ( normalize (myTransform xAxis)
  , normalize (myTransform yAxis)
  , normalize (myTransform zAxis)
  )

@TheSeamau5
Copy link

To make it more concrete, how should one represent a 4x4 matrix efficiently? I imagine a tuple is probably as efficient as it gets?

Yup, matrices are just typed arrays flattened to 1d.

Thing is that tuples are (from what I can tell) always JS objects. I think that this could be an optimization worth having. Like, an n-tuple of ints or floats are just typed arrays. Cuz then you could use destructuring to work with these things in Elm as opposed to like elm-linear-algebra use functions like getX and getY...

Imagine:

p = (1 ,2, 3)
q = (4, 0, 5)

(a,b,c) = p `add` q -- (5, 2, 8)

and that uses typed arrays. I wonder if you could push the optimization further to make records into typed arrays and somehow have the fact that the fields are called "x", "y", and "z" somewhere hidden in the runtime

@rehno-lindeque
Copy link
Author

I wonder if you could push the optimization further to make records into typed arrays and somehow have the fact that the fields are called "x", "y", and "z" somewhere hidden in the runtime

This would be great. I can't imagine that the Elm compiler would ever optimize away records' keys without some kind of prompting (in order to generate readable JS code), but perhaps it might generate more efficient/packed data structures for tuples.

@TheSeamau5
Copy link

I think that using tuples for math could be great given how common vector math is. It's sometimes in UI front-end work (with animations) but it's especially present with games, and people love writing games in Elm.

Given how common it is, we could super special case tuples for math operators. Like:

p = (1, 2, 3)
q = (3, 1, 2)

r = p + q -- (4, 3, 5)

We could have all the usual suspects when it comes to operators:

(+) : N-Tuple number -> N-Tuple number -> N-Tuple number
(-) : N-Tuple number -> N-Tuple number -> N-Tuple number
(*) : N-Tuple number -> N-Tuple number -> N-Tuple number
(/) : N-Tuple number -> N-Tuple number -> N-Tuple number
(.) : N-Tuple number -> N-Tuple number -> N-Tuple number

cross : 3-Tuple number -> 3-Tuple number -> 3-Tuple number

and we could also extend them to matrices where matrices are nested tuples.

Like:

type alias Matrix4 = 
  ( (number, number, number, number)
  , (number, number, number, number)
  , (number, number, number, number)
  , (number, number, number, number)
  )

and until the day Elm gets something like Typeclasses (if it ever gets it), the compiler treats these math operators the same way ++ is treated. Like they're super special and they can work on a few types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment