Hacker News new | past | comments | ask | show | jobs | submit login

It's a shame you feel that way. Modifying an element of an array in Haskell is easy and the blog post gives almost exactly correct code, but for some reason places it under the title "we run into trouble when we realize there’s no built-in mutating assignment". But we don't run into trouble! Mutable arrays are provided in Haskell! Haskell has mutation! Here's the code

    import Data.Array.MArray
    import Data.Array.IO
    size = 10
    (!) = readArray
    main :: IO ()
    main = do
      -- 1. Declare the array
      arr <- newArray ((1,1), (size,size)) undefined
      let _ = arr :: IOArray (Int,Int) Integer
      -- 2. Initialize the array to 0
      sequence_ $ do
        i <- [1..size]
        j <- [1..size]
        return $ writeArray arr (i, j) 0
      -- 3. Set the diagonal to 1
      sequence_ $ do
        i <- [1..size]
        return $ writeArray arr (i, i) 1
      -- 4. Print the array
      sequence_ $ do
        i <- [1..size]
        j <- [1..size]
        return $ do
          arr_i_j <- arr ! (i,j)
          putChar $ if arr_i_j == 0
                    then '0'
                    else '1'
          if j == size
            then putChar '\n'
            else return () 

    > main
I've no idea why the blog post is written in such long winded style. Mutation in Haskell is not difficult!

> I've no idea why the blog post is written in such long winded style. Mutation in Haskell is not difficult!

For a second there, I honestly thought you were being sarcastic. I'd consider the example you just gave to be extremely "long winded" and "difficult" for what it does. If I ported that code line-for-line to Ruby, for example, the whole thing would look like this:

  size = 10

  # Initialize the array to 0
  arr = Array.new(size) { Array.new(size, 0) }

  # Set the diagonal to 1
  (0...size).each{|i| arr[i][i] = 1 }

  # Print the array
  puts arr.map{|row| row.join}.join("\n")

That's not really a line for line port. Or even block for block since yours is never uninitialized. The original code was designed to basically mimic writing bad C in Perl, with an uninitialised array that you then explicitly looped over to initialise to zero, and then modified the diagonal. That sort of thing is difficult on purpose in Haskell.

On the other hand...

  main = mapM_ (putStrLn . foldMap show) [bool 0 1 . (==x) <$> [1..size]| x<-[1..size]]

  x .* y = replicate y x
  main = mapM_ (putStrLn . foldMap show) [0 .* x ++ 1 : 0 .* (size-x)| x<-[0..size-1]]
Or if you really do want mutation shorthand (because you're a crazy person) just build it:

  a .// l = mapM_ (uncurry $ writeArray a) l

  pickFn f g (b,v) | b = g v | otherwise = f v

  main = do
    arr <- newArray ((1,1), (size,size)) 0
    arr .// [((x,x),1)|x<-[1..size]]
    mapM_ (pickFn putStr putStrLn . (((==size).snd) *** show)) $ getAssocs arr
It's not pretty, but it's not supposed to be. Do you know what is pretty? Doing things the right way.

  sudo apt-get install haskell-stack
  stack setup
  stack ghci --package matrix
  import Data.Matrix
  print $ identity 10
And of course there's similar for Ruby.

Pop quiz:

Do you genuinely believe I wrote the code the way that I did because it's simplest way of initialising an identity matrix in Haskell?

FWIW. (Haskell doesn't have a convenient built-in function for printing arrays. Guilty as charged!)

    size = 10
    main = do
      -- Initialise the array to 0
      arr <- newArray ((1,1), (size, size)) 0
      let _ = arr :: IOArray (Int, Int) Int

      -- Set the diagonal to 1    
      forM_ [1..size] $ \i -> writeArray arr (i,i) 1
      -- Print the array
      forM_ [1..size] $ \i -> do
        forM_ [1..size] $ \j -> do
          a <- readArray arr (i, j)
          putChar (if a == 0 then '0' else '1')
          when (j == size) (putChar '\n')

Thanks, that looks much nicer and more in-line with what I'd expect from an example intended to demonstrate how easy a simple task is in a specific language.

As an aside, Ruby doesn't have a built-in function for printing arrays either (unless you count .inspect or .to_s, which let you print _anything_). The code I gave formatted the array as a string using some simple data manipulation functions before printing it; it was intended to match the output of the program you provided.

But IO (or ST) style is also unidiomatic and simply too cumbersome. You are just not going to write "writeArray arr (i,j) k" instead of "arr[i][j] = k", and more importantly,

    v <- readArray arr (i,j-1)
    w <- readArray arr (i-1,j)
    writeArray arr (i,j) (v+w)
instead of

    arr[i][j] = arr[i][j-1] + arr[i-1][j];
No way you're doing that over and over again for any substantial performance-oriented code (where you absolutely need mutable arrays).

I don't think IO is unidiomatic at all, although 7 years ago I might have agreed. First if we really need to we can write haskell that looks a lot like C. Secondly if I find myself in a situation where I need to write a little block of code that just doesn't lend itself to being written in Haskell I have no problem writing it in C. If I was implementing some numerical algorithm this is what I would do. Indeed haskell bindings to things like BLAS to just this. The benefit of marking all side effects with IO is an easy to read codebase, easy refactoring and of course Software Transactional Memory!

Doing this kind of effect sequencing in Haskell is annoying indeed. A radical library can mitigate that but that would be really unidiomatic.

Could be nice to have sugar like expr[!x] that expand to: x >>= \genName -> .. expr[genName] ..

So you could write: writeArr arr !(readArr ..) !(readArr ..)

Then if you add an operator to do the reading it becomes quite reasonable.

Well, you can add operators, though you almost certainly should think about more idiomatic solutions first, it's entirely possible to write operators to allow a statement like:

  arr .~ (i,j) .= arr ! (a,b) + arr ! (x,y)
You make orphan instances for Indexed and Num, and then .~ is just a slight tweak on the adjust function that Indexed provides and .= is literally a direct synonym for $ that just looks better in this context. This is actually more flexible than most languages, because now (arr .~ (i,j)) or (.~ (i,j)) can be named and applied to multiple things should that be desirable for some reason. Also note that this code is very weird, as arr is not an array, but an IO action describing how to produce one. Operations on it actually produce diffing instructions to be applied elsewhere. I have also not tested the performance.

These exact things are not in base because they are discouraged and not supposed to be easy. Note that the lens library provides operators more or less just like this for a wide variety of data types, in a safe and composable way.

Taking IO actions just to do the bind for the caller goes against the benefits you usually get from purity.

OK, I know this isn't nice but it's already part of the way there

    writeArray arr (i,j) =<< ((+) <$> readArray arr (i,j-1) <*> readArray arr (i-1,j))

This is not only not nice, it's just a bad idea. (and I understand it!)

I think you may have misinterpreted my comment.

It's not unidiomatic at all, and if you need to do it, you'll do it.

{caveat: I am pretty sure that there are other ways to write the Haskell code at a higher level. I wrote this not as a rant about Haskell, but because I have been thinking about 'big versus small' languages, recently.}

For me, I want a big language, not more correct assembly or C. An identity matrix in Racket is three lines:

  #lang racket
  (require math/matrix)
  (identity-matrix 10)
Or more iteratively:

  #lang racket
  (require math/matrix)
    (build-list 10 (lambda (x) (values 1))))
Or more generically:

  #lang racket
  (require math/matrix)
  (build-matrix 10 10
    (lambda (x y)
      (if (= x y)
        (values 1)
        (values 0))))
The choice three different abstractions in fewer lines of code than the single Haskell abstraction. For all its type safety the Haskell abstraction is down and dirty Von Neumann style sequential memory access. Looping over the elements of a matrix is pretty far away from the useful abstractions of matrices.

To put it another way, i's and j's are good variable names for loops and bad variable names for matrices...If I have to iterate over elements of a matrix, I really want m's and n's. But that conflicts with semantics of a conventional looping construct.

> caveat: I am pretty sure that there are other ways to write the Haskell code at a higher level. I wrote this not as a rant about Haskell, but because I have been thinking about 'big versus small' languages, recently

> The choice three different abstractions in fewer lines of code than the single Haskell abstraction

These two parts of your comment don't seem to mesh :)

In 'big languages' I am thinking more about how much 'talking about' domain specific topics looks like 'talking about' the topics covered in the core language. Or maybe about how easy it is to make 'talking about' domain topics look like 'talking about' core topics. Maybe it's the degree to which the reductionist process of dividing up code into files is typically reflected when writing code.

Some languages like Python default to explicitly referencing the module, that is the happy easy path leads to writing "math.floor(x)," not "floor(x)." That's even after I write the mandatory "import math." C is probably close to one end of the spectrum, and at the other end of the spectrum are languages like Wolfram, where:

   [Entity["City", {"NewYork", "NewYork", "UnitedStates"}], 
    Entity["City", {"LosAngeles", "California", "UnitedStates"}]]
Racket is probably closer to Wolfram, and Haskell closer to C. I think the C end is more likely the more a language tries to solve problems that programs don't have before they are written...e.g. speed and type errors. The Wolfram end tries to solve problems that programmers have before programs are written...e.g. what do I want to say.

Wishful thinking is often Abelson's starting point in the SICP lectures. That's a bit at odds with the philosophy of some languages.

You're probably right that the presentation here is misleading. Most common things like arrays, dictionaries, sets, and more don't really present any trouble in practice. You only use the techniques in my article when structuring your program more broadly, and arrays were just an example.

I'll make a note that for arrays specifically, someone took the trouble to make a mutable version that's usable in IO. Nobody would ever read and write references to an immutable version over using just MArray, but my post might mislead them into doing just that.

not that this is code golf, and not that the following is even mutation, but I couldn't help trying out the "for" syntax in Elixir, which this seemed like a good job for:

    one_or_zero = fn(x,y) -> if x==y, do: "1", else: "0" end
    for i <- 1..10 do
      for j <- 1..10 do
        one_or_zero.(i,j) |> IO.write

Applications are open for YC Winter 2023

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact