"Sick of Ruby, dynamic typing, side effects, and basically object-orientied programming"
He's sick of OOP in Ruby, not of OOP in general. Ruby's dynamic typing means you have one type, "the dynamic type". The compiler leaves you entirely on your own as static analysis is impossible. It means many more tests, assertions, and runtime error conditions. Most OOP techniques have evolved out of, and rely on strong static typing, Ruby isn't that kind of language.
OOP in Ruby is the inevitable result of many Ruby projects growing in scope and size. When you reach that point, a better way is to move part of the project out of Ruby and into a language that can handle that scope and size.
P.S.: Any communication with the outside world is a side effect (ex. network, file, database I/O), so sick of it or not, you better get used to it. Haskell masking side effects as monads through a language loophole isn't making things significantly better.
P.S.: Any communication with the outside world is a side effect (ex. network, file, database I/O), so sick of it or not, you better get used to it. Haskell masking side effects as monads through a language loophole isn't making things significantly better.
Eh what? Even as a Haskell non-user, this just doesn't make any sense.
a) Haskell doesn't "mask" side effects
b) The fact that monads are involved is completely irrelevant
c) It's not a language loophole
a) side effects are "masked" in Haskell in that a function that is generic over any type of monad may or may not incur side effects at run-time, depending on which actual monad it is used with.
b) see above - because monads as a type class exist, a function may be written with a type signature that does not indicate in any way that it will be eventually used over IO.
c) the fact that the Haskell denotational semantics are entirely silent over what happens at run-time with the IO monad is definitely a loophole. From a birds view, a Haskell program is a giant expression that reduces to an object of type IO, and what happens with that object is entirely outside of the language semantics.
a-b) The objects of type IO are values, just as any other monad. It is true that polymorphic functions can be instantiated with type IO, just as they can be of any other type. How you think this supports the assertion that side-effects are "masked" is beyond me.
c) The language boundary of Haskell typically ends at the production of an IO value. You can easily give an operational semantics to many IO operations, and in fact there are papers that do this for simple IO operations such as putStr. It's not possible to give a full operational semantics for IO in any language, not just Haskell. Of course you are going to model console I/O, references, exceptions, concurrency features (all of which have been done). But these aren't the only kinds of side effects. Does your model account for the network, and all its complexities? Does your model account for power failures? Does your model account for cosmic rays? Most models don't, but nobody calls them loopholes, nobody implies it's a big cover-up. Every model is going to have a boundary, and Haskell's seems good enough to me. It's no more undefined than, say, ML.
a and b don't actually matter, because what matters is how the function is called. If given some `f :: Monad m => m -> m`, then you can only call f with IO as the monad from an already impure context. For example, you can't call f with IO from inside a function (g :: a -> b).
The purpose of IO in Haskell is to explicitly mark side effects, because they cannot be arbitrarily composed in the way pure functions can. IO represents a one-way boundary in that you can turn some pure computation into an impure one (a -> IO a), but there is no way of "extracting" that computation back from IO (i.e, there exist no (IO a -> a)). That "monads" are used to do this is useful because they provide the (a -> IO a), and happen to have a convenient function for chaining computations (IO a -> (a -> IO b) -> IO b).
How IO is defined is up to the implementation, and not in the scope of the language - different implementations could use a different representation for IO - what matters is that it must be defined in a way that one cannot define an (IO a -> a).
On "a Haskell program is a giant expression that reduces to an object of type IO", this is really nothing to do with Haskell, but a consequence of how we've built our operating systems and how the defacto meaning of "program" these days is equivalent to an "executable file". Traditionally "program" was much more abstract and could refer simply to any piece of code, such as a (pure) haskell function. We can consider any Haskell function to be a "program" in itself. If we had an environment from which we launched processes which provided all the command line switches and environment variables as arguments, we could easily omit the "IO" from our "main", if the rest of our code was pure. (On a side note, this is precisely what early versions of Haskell did, before monadic IO became practical)
The point of the Haskell style is it makes it easy to control side effects. If you don't care when I/O happens, you can just do everything inside the I/O monad and pretend you're writing Ruby. But if you want to know which functions do I/O and which don't (which can help e.g. test appropriately), Haskell gives you the tools to keep track of that in a lightweight way.
Comments
The original headline is this:
"Sick of Ruby, dynamic typing, side effects, and basically object-orientied programming"
He's sick of OOP in Ruby, not of OOP in general. Ruby's dynamic typing means you have one type, "the dynamic type". The compiler leaves you entirely on your own as static analysis is impossible. It means many more tests, assertions, and runtime error conditions. Most OOP techniques have evolved out of, and rely on strong static typing, Ruby isn't that kind of language.
OOP in Ruby is the inevitable result of many Ruby projects growing in scope and size. When you reach that point, a better way is to move part of the project out of Ruby and into a language that can handle that scope and size.
P.S.: Any communication with the outside world is a side effect (ex. network, file, database I/O), so sick of it or not, you better get used to it. Haskell masking side effects as monads through a language loophole isn't making things significantly better.
Eh what? Even as a Haskell non-user, this just doesn't make any sense.
a) Haskell doesn't "mask" side effects b) The fact that monads are involved is completely irrelevant c) It's not a language loophole
a) side effects are "masked" in Haskell in that a function that is generic over any type of monad may or may not incur side effects at run-time, depending on which actual monad it is used with.
b) see above - because monads as a type class exist, a function may be written with a type signature that does not indicate in any way that it will be eventually used over IO.
c) the fact that the Haskell denotational semantics are entirely silent over what happens at run-time with the IO monad is definitely a loophole. From a birds view, a Haskell program is a giant expression that reduces to an object of type IO, and what happens with that object is entirely outside of the language semantics.
a-b) The objects of type IO are values, just as any other monad. It is true that polymorphic functions can be instantiated with type IO, just as they can be of any other type. How you think this supports the assertion that side-effects are "masked" is beyond me.
c) The language boundary of Haskell typically ends at the production of an IO value. You can easily give an operational semantics to many IO operations, and in fact there are papers that do this for simple IO operations such as putStr. It's not possible to give a full operational semantics for IO in any language, not just Haskell. Of course you are going to model console I/O, references, exceptions, concurrency features (all of which have been done). But these aren't the only kinds of side effects. Does your model account for the network, and all its complexities? Does your model account for power failures? Does your model account for cosmic rays? Most models don't, but nobody calls them loopholes, nobody implies it's a big cover-up. Every model is going to have a boundary, and Haskell's seems good enough to me. It's no more undefined than, say, ML.
a and b don't actually matter, because what matters is how the function is called. If given some `f :: Monad m => m -> m`, then you can only call f with IO as the monad from an already impure context. For example, you can't call f with IO from inside a function (g :: a -> b).
The purpose of IO in Haskell is to explicitly mark side effects, because they cannot be arbitrarily composed in the way pure functions can. IO represents a one-way boundary in that you can turn some pure computation into an impure one (a -> IO a), but there is no way of "extracting" that computation back from IO (i.e, there exist no (IO a -> a)). That "monads" are used to do this is useful because they provide the (a -> IO a), and happen to have a convenient function for chaining computations (IO a -> (a -> IO b) -> IO b).
How IO is defined is up to the implementation, and not in the scope of the language - different implementations could use a different representation for IO - what matters is that it must be defined in a way that one cannot define an (IO a -> a).
On "a Haskell program is a giant expression that reduces to an object of type IO", this is really nothing to do with Haskell, but a consequence of how we've built our operating systems and how the defacto meaning of "program" these days is equivalent to an "executable file". Traditionally "program" was much more abstract and could refer simply to any piece of code, such as a (pure) haskell function. We can consider any Haskell function to be a "program" in itself. If we had an environment from which we launched processes which provided all the command line switches and environment variables as arguments, we could easily omit the "IO" from our "main", if the rest of our code was pure. (On a side note, this is precisely what early versions of Haskell did, before monadic IO became practical)
The point of the Haskell style is it makes it easy to control side effects. If you don't care when I/O happens, you can just do everything inside the I/O monad and pretend you're writing Ruby. But if you want to know which functions do I/O and which don't (which can help e.g. test appropriately), Haskell gives you the tools to keep track of that in a lightweight way.