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?
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.
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.
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.
The main motivation for me is avoiding generating a lot of libraries with "tuple boilerplate" before they get written. I might be happy even with just restricting tuple syntax to pairs for the time being in order to avoid falling into that trap and encouraging people to use records instead.