What you describe in your 3rd paragraph is precisely the ST monad.
Not exactly. The ST monad supports local mutable state, essentially turning a computation that uses state internally into something that looks pure from the outside. I’m looking for something more general where, for example, a function could modify state outside its own scope, but only if it advertised explicitly which stateful resources it would read and/or write, so the language could enforce constraints or issue warnings if various kinds of undesirable and/or non-deterministic behaviour would result.
And really, I want to generalise the concepts of resources and effects on them much more widely. A stateful resource might be a mutable variable, but it could just as well be a file, a database, a single record within a whole database, or anything else I care to model in that way. An effect might be reading or writing part of the state, but it might also be opening or closing a file, or beginning, committing or rolling back a DB transaction. I want a language that can understand the concept that a file must not be read or written before it is opened, or that a database transaction must always be committed or rolled back once it has begun, and warn me if any code paths could permit an invalid order of effects. I don’t just want to throw half my code into the IO monad and hope for the best.
I want a language where I can pass a reference to a large data structure into a parallel map function and get a compile-time error because, somewhere in the function I was applying using the map, there was an attempt to perform a mutating operation on that shared data without sufficient safeguards. But I want to write almost identical code and have the effects automatically co-ordinated because the language understands that concurrent access is possible so synchronisation is necessary, but the spec for the parallel map says that non-determinism is permitted.
And crucially, I want a language where even simple code that should look like this:
def fib(n):
if n < 2:
return n
x, y = 0, 1
for i in range(n - 1):
x, y = y, x + y
return y
does not wind up looking like this (from [1]):
fibST :: Integer -> Integer
fibST n =
if n < 2
then n
else runST $ do
x <- newSTRef 0
y <- newSTRef 1
fibST' n x y
where fibST' 0 x _ = readSTRef x
fibST' n x y = do
x' <- readSTRef x
y' <- readSTRef y
writeSTRef x y'
writeSTRef y $! x'+y'
fibST' (n-1) x y
Your last complaint is not fundamental to the language: it's just a matter of library design. I guess Haskellers don't like state very much in any guise, so having awkward syntax for it is only natural. Both programs are fundamentally ugly, so having it be superficially ugly as well is no big loss.
However, if you really wanted to, you could have almost C-like syntax[1]. The demo in that particular blog post has its own issues, but there is no fundamental reason why you couldn't apply some of those ideas to the normal ST API. I suspect that people simply don't care enough.
Both programs are fundamentally ugly, so having it be superficially ugly as well is no big loss.
Perhaps we’ll just have to amicably disagree on that point. I think making code superficially ugly is a huge barrier to adoption that has held back otherwise good ideas throughout the history of programming.
Many an innovative programming style or technique has languished in obscurity until some new language came along and made the idea special and provided dedicated tools to support it. Then it becomes accessible enough for new programmers to experiment with the idea, and the code becomes readable enough to use the idea in production, and over time it starts to change how we think about programming. We push the envelope, probably developing some ingenious and useful but horrible abuses of the idea along the way, until we figure out how to make those new ideas available in a cleaner and more widely accessible way and the cycle begins again.
I suspect that people simply don't care enough.
Most people are programming in languages that don’t have the problem in the first place, so they don’t need to care.
> I think making code superficially ugly is a huge barrier to adoption that has held back otherwise good ideas throughout the history of programming.
Haskell tries to make elegant code look nice and inelegant code look ugly. When you see something ugly like the example above this is a sign that there would be some more elegant way of writing it. That certainly holds in this case as you don't need an mutation to do the fib sequence.
For something as simple as fibonacci, you wouldn't use the state monad machinery, but go directly:
fib' :: Integer -> Integer
fib' n = iterate (\(x,y) -> (y,x+y)) (0,1) !! n
The state monad helps you keep more complicated code composable. But it does have some syntactic overhead, that makes it lose out in simple examples. (Though with a bit of golfing, you could get syntactically closer to the Python example. For example putting the tuple (x,y) into an STRef instead of having two of them, employing Applicative Functor style in some places, and using a loop-combinator from the monad-loops package.)
Concepts like your file state machine can be achieved using indexed monads and dependent types, though that's still fairly black magic. There's no doubt that the IO monad should have more structure, though, and ST is just one way to achieve that. There isn't a globally recognized standard for providing more structure, but there are a lot of fairly common practices (see IOSpec or free monads).
The syntactic convenience is unbeatable, of course. Monads are only first class syntactic notions up to `do` notation. Someone is bound to say "let just implement that in Template Haskell", but nobody really argues that TH macros are the same as lispy ones.
The syntactic convenience is unbeatable, of course.
Exactly.
I’m not in any way criticising all the research and ideas coming out of the functional programming community. Playing with Haskell is driving all sorts of interesting ideas.
I’m just saying I don’t think the mainstream is going to give up a whole bunch of readily accessible and useful programming techniques any time soon, just to gain the benefits of a radically different programming style that sound rather theoretical to Bob the Developer. This is true even if the reality might be that in time Bob’s bug rate would drop dramatically and he would develop new features in a fraction of the time.
I think it’s far more likely that the more accessible and obviously useful parts of that radically different programming style will get picked up over time, and that the underlying general/theoretical foundations will be more useful to the people building the specific/practical tools than to the people using them.
Today, we’ve been discussing how to implement even quite simple effect-related concepts in a functional programming context in terms of (borrowing from a few of your posts in this thread) products and compositions of applicative types, free monads, monad transformers, indexed monads, dependent types, and the problems of uncertain denotation. Poor Bob just wants to get an error message if he forgets to commit his transaction and leaves the DB locked or if he might be dereferencing a null pointer in his network stack. :-)
That's utterly fair. I don't expect Bob the programmer to adopt inconvenient things. I think having static assurances of the like you were asking for will probably never quite be in Bob's purview either, though.
To clarify, I don't think Haskell is the ultimately useful embedding of many of these concepts. I just also don't think there's that much hand holding possible. You start to run up against the limits of inference and decidability in this space. Until Bob the programmer start to meaningfully think about the way to build the effects of his language around different contexts then there's a limit to how many guarantees a compiler is going to be able to make.
Not exactly. The ST monad supports local mutable state, essentially turning a computation that uses state internally into something that looks pure from the outside. I’m looking for something more general where, for example, a function could modify state outside its own scope, but only if it advertised explicitly which stateful resources it would read and/or write, so the language could enforce constraints or issue warnings if various kinds of undesirable and/or non-deterministic behaviour would result.
And really, I want to generalise the concepts of resources and effects on them much more widely. A stateful resource might be a mutable variable, but it could just as well be a file, a database, a single record within a whole database, or anything else I care to model in that way. An effect might be reading or writing part of the state, but it might also be opening or closing a file, or beginning, committing or rolling back a DB transaction. I want a language that can understand the concept that a file must not be read or written before it is opened, or that a database transaction must always be committed or rolled back once it has begun, and warn me if any code paths could permit an invalid order of effects. I don’t just want to throw half my code into the IO monad and hope for the best.
I want a language where I can pass a reference to a large data structure into a parallel map function and get a compile-time error because, somewhere in the function I was applying using the map, there was an attempt to perform a mutating operation on that shared data without sufficient safeguards. But I want to write almost identical code and have the effects automatically co-ordinated because the language understands that concurrent access is possible so synchronisation is necessary, but the spec for the parallel map says that non-determinism is permitted.
And crucially, I want a language where even simple code that should look like this:
does not wind up looking like this (from [1]): [1] http://www.haskell.org/haskellwiki/Monad/ST