Maybe something roughly like this:
data Matrix a = Matrix
{ matrixDimensions :: [Int]
, matrixCells :: [a]
} deriving (Show)
fromList :: [a] -> Matrix a
fromList xs = Matrix [length xs] xs
instance Functor Matrix where
fmap f (Matrix dims xs) = Matrix dims $ fmap f xs
instance Applicative Matrix where
pure x = Matrix [] [x]
Matrix fds fs <*> Matrix xds xs =
Matrix (fds ++ xds) (fs <*> xs)
instance Monad Matrix where
Matrix ds xs >>= f = Matrix (ds ++ rds) rs
where
Matrix rds rs = case f <$> xs of
[] -> Matrix [] []
ms@(Matrix frds _:_) ->
Matrix frds (concat $ matrixCells <$> ms)
ghci> [(x, y) | x <- fromList [1..3], y <- fromList [2..5] ]
Matrix {matrixDimensions = [3,4], matrixCells = [(1,2),(1,3),(1,4),(1,5),(2,2),(2,3),(2,4),(2,5),(3,2),(3,3),(3,4),(3,5)]}
If you want it to be pretty printed into a nice-looking table, you can define your own Show instance.This is probably not the best way to implement it, though. Notice that I use the first Matrix returned by `f` in the Monad instance definition and assume that all subsequent matrices are of the same dimensions. The problem with fixing that is that we'd have to fix the dimensions into the type and that would prevent us from writing a Monad instance that would work for your example.
I don't know Julia, but I think the reason why this is not a problem for it is because neither x nor y are in scope for each other's definition:
julia> [ (x,y) for x in 1:3, y in 1:x ]
ERROR: UndefVarError: x not defined
Stacktrace:
[1] top-level scope
@ REPL[1]:1
julia> [ (x,y) for x in 1:y, y in 1:3 ]
ERROR: UndefVarError: y not defined
Stacktrace:
[1] top-level scope
@ REPL[2]:1
while x would be in scope for y's definition in Haskell, allowing the y dimension to vary by value of x. I guess the best one can do in terms of safety for that case is turn it into a runtime error in Haskell.If one wants to handle Matrices with better type-safety it's probably best to give up on comprehensions for it, make the Matrix type more descriptive, and use other operators.