Hacker News new | ask | show | jobs
by ayberkt 3661 days ago
It makes me sad how people think imperative programming is a "natural" way to think. I have known lots of people who resist using functional languages for serious projects, marginalizing the "functional" way of thinking as though thinking in terms of "labeled boxes with data in them" (Von Neumann architecture) is some sort of more normal way to think.

Before computers people modeled the real world using a Haskell-like pseudocode called "mathematics".

7 comments

I find both mental models useful. For example, "accumulate all of the monoids in this collection" (https://hackage.haskell.org/package/base-4.9.0.0/docs/Data-F...) makes total sense as a functional operation. You have a collection and some pure operations you want to apply to combine it all into one thing.

But then, sometimes you just want to iterate across a list of directories, read some files, and put some results in lists. Tail-recursion here feels dumb. Like, dammit, I'm the human and you're the computer, I know you can turn a sequence of instructions into a tail recursive loop for me, so don't make me think.

Haskell, due to its effectful / monadic vs. pure syntax distinction isn't terribly great here. Ideally I'd write all of my code in an "imperative style" all the time, but a row-typed effect inference system would determine which functions were pure, or in IO, or async, or in ST, or threw exceptions, etc. Koka [1] is an interesting language that explores some of these ideas.

[1] http://research.microsoft.com/en-us/projects/koka/

Some other projects that are doing cool things in this space are Idris[1] and F-Star[2] (the latter is also a Microsoft Research project). I am particularly impressed by Idris's algebraic effect approach, since it gives you a general effect system that combines the ability to easily extend that monad transformer approaches provide with the ease of use and inference of F*'s effect system.

I think the theory behind algebraic effects is a key step in making it easy to develop code with more stringent and customized safety properties than is available in languages like Rust.

[1]: http://www.idris-lang.org/ [2]: https://www.fstar-lang.org/

> Ideally I'd write all of my code in an "imperative style" all the time, but a row-typed effect inference system would determine which functions were pure, or in IO, or async, or in ST, or threw exceptions

Yes, this would be a really nice approach (and it's not one that Haskell lovers would be upset by, BTW).

I would be upset if it meant no visible distinction between effectful and non-effectful calls. When refactoring it's really important to know when the order of calls is significant vs when it's incidental.
Agreed, I just recently took some recursive F# code from a photobooth (display countdown, async sleep, take photo, download from camera, recur with new byte[] list)->reverse list, and rewrote it it an imperative loop appending to a normal List<byte[]> and the code is much more intuitive.

Some things are more intuitive as a list of things to do and changes to make.

Aren't IO Actions just a list of things to do and changes to make? I'm asking genuinely to try to understand the difference.
Yes they are, but I think "for loops" and "mutable lists" a'la Java's ArrayList and Vector and C#'s List<T> don't exist even in IO (I'm not a Haskell expert). That kind of thing has to be done with recursion. They exist in F# but are not idiomatic. However sometimes they're more intuitive (to me at least).
forM is a for each loop over anything iterable (Traversable in Haskell) that will execute a given monadic action on each element in the collection. A more traditional for loop is this over a range of integers or whatever else you feel like.

    forM (range 5) println
Mutable collections exist in various forms depending on what kind of mutability you want. For example, there's transactional mutability that gives you mutable data structures inside an STM but forbids IO. And there's IO mutability that removes all the bounds but restricts you to the IO Monad. TVars, MVars, and the various kinds of Refs are what you're looking for here.
If someone is asking for mutability for reasons of performance and clarity, giving them STM is like a kick in the teeth.
Though maybe I stand corrected. I revisited that. Here's what I had initially:

(include the following stubs to compile):

  let upload _ = async.Return()
  let takePhoto i = async.Return [|0uy|]
  // i stands for "photo i of N", a required parameter for our workflow
---

  let runSequence n =
    async {
      let rec runSequenceRec (framesSoFar: byte[] list) =
        async {
          if framesSoFar.Length = n then
            return List.rev framesSoFar
          else
            let! bytes = takePhoto framesSoFar.Length
            do! upload bytes
            return! runSequenceRec (bytes::framesSoFar)
        }
      return! runSequenceRec []
    }
Nasty dual-level async calls, list reversal, recursion, if/else, an initializing []. A lot of visual junk that takes away from what the code is actually doing. I rewrote it imperatively to this:

  let runSequence' n =
    async {
      let frames = System.Collections.Generic.List<byte[]>()
      for i = 0 to n do
        let! bytes = takePhoto i
        do! upload bytes
        frames.Add bytes
      return frames
    }
Much cleaner, you can see exactly what's going on. I was fine with that, and that's where I left it until now.

However reading some of the Haskell comments, I thought there might be something better. So I tried replicating Haskell's forM:

  let forNAsync f n =
    let rec runOnce (soFar: _ list) =
      async {
        if soFar.Length = n then
          return List.rev soFar
        else
          let! result = f soFar.Length
          return! runOnce (result::soFar)
      }
    async.ReturnFrom(runOnce [])
Okay, not pretty but it's a library function so we should never have to look at it again. With that in hand though, we can define:

  let runOneShot i =   
    async {
      let! bytes = takePhoto i
      do! upload bytes
      return bytes
    }

  let runSequence'' = forNAsync runOneShot
This is terrific. Shorter and cleaner than the imperative method, preserves immutability throughout, and composes easily.

If you were to try to compose `runOneShot` from the imperative method, you'd end up with this, because of your frames variable

  let runSequence''' n =
    async {
      let frames = System.Collections.Generic.List<byte[]>()
      for i = 0 to n do
        let! bytes = runOneShot i
        frames.Add bytes
      return frames
    }
Or if you wanted to try to rework the `runOneShot` to include the frames variable, it wouldn't be much easier:

  let runOneShot'''' i (frames: System.Collections.Generic.List<byte[]>) = 
    async {
      let! bytes = takePhoto i
      do! upload bytes
      frames.Add bytes
    }

  let runSequence'''' n =
    async {
      let frames = System.Collections.Generic.List<byte[]>()
      for i = 0 to n do
        do! runOneShot'''' i frames
      return frames
    }
So, maybe things that seem imperative do work better functionally. You've just got to create some combinators that let you hook them up.
I find writing recursive code is often very short and easier to read and show intent than the same code written non recursively.
Additionally, in languages with good, convenient support for functional programming like Haskell and Scala, I find that I tend to write lots of short functions, often making use of higher-order functions to decompose code into really simple, and potentially reusable, pieces. And like you say, this really helps to show intent. The same sort of thing, in fact, that object-oriented programmers try to do with design patterns such as the Strategy pattern - but with fewer lines of code.
That is definitely right. For things involving the machine, doing things declaratively is wrong.

Also, thanks for telling about Koka. It sounds very interesting!

I'm not sure I agree. Functional Reactive Programming is a declarative way of doing user interaction, but it's still a computer science research topic (Elm used to claim to be FRP, but has now decided to move away from its old allegedly FRP-like style). I need to play around with the latest FRP research libraries some time!

In addition there is also machines/pipes/conduits, which all mix imperative I/O with declarative style code in an interesting and new way.

FRP is awesome and is an example of how something previously thought of as an inherently imperative problem can be much easily done declaratively; we completely agree on this!

What I was talking about is: almost all lower-level abstractions are designed with the view of computer as a state machine. If we are solving a problem at an abstraction low enough to make us deal with this analogy, it is easier not to have a declarative layer in between.

machines/pipes/conduits are all very interesting and I was going to mention them too, but I still don't think they solve this problem in the way OP was referring to.

Before computers, far more people did bookkeeping or followed recipes or read sheet music than did abstract mathematics. I think one can reasonably say that lists of instructions that change mutable variables/objects is one of the most natural ways people can operate. Anyone learning a task essentially does this in the real world. It may indeed be terrible when done at a large scale. But there are a lot of people who can naturally understand these.

Immutable data may be a great idea. But is it the first that occurs to anyone? I have an MA in mathematics and the idea of mutable variables never seemed strange in the process of learning to program.

But even more, for a lot of people, even algebra isn't a natural way of thinking. What's natural is a series of instruction about how perform arithmetic. Learning what an abstract variable means is a hurdle for a lot of students.

The thing is that "natural", in this context, just means "what occurs to you first, all things being equal".

You may have a good and correct argument that learning Haskell is the right way to do things, that the costs of learning are more than exceed by the benefits.

But I don't think you can argue people into having functional approaches be the first thing that occurs to them.

I think it's impossible to disentangle the reality of common programming languages and how they work from what is inherently more "natural". The bottom line is programming is not natural, not in any way shape or form. It's hard to learn imperative programming, it's hard to learn functional programming, and it's really hard to learn object-oriented programming (well). If all languages were functional I don't think that would really be harder than learning BASIC, people would just accept that is what programming is and get on with their lives.

I acknowledge that imperative programming is more analogous to real world tasks, but then as soon as you try to write your first program you run headlong into the fact that programming is nothing like other tasks you have done before. The tricks you have to learn to make imperative programming work are not really any easier than the tricks for other paradigms, and in fact you can mix and match from various paradigms to great effect (see: ruby).

"...it's really hard to learn object-oriented programming (well)."

You have no idea how desperately I wish more people understood that. I could swear that every introductory programming book produced in the last years was either written by someone who either completely forgot how hard this material was to originally master, or who was just some freak of nature who just intuitively grasped it.

Please, please, please, give your students a solid grounding in procedural (or even functional!) programming before teaching OOP. You can teach them about objects and how to use them, but please stop making people write entire classes before they have at minimum a month of coding exercises behind them! (Universities that teach CS101 in Java, I'm looking at you.)

Anyway, I strongly agree with you and reached for the upvote button, and ended up downvoting you instead. Stupid touchscreens.

There, I undid your downvote with my upvote.
but then as soon as you try to write your first program you run headlong into the fact that programming is nothing like other tasks you have done before. The tricks you have to learn to make imperative programming work are not really any easier than the tricks for other paradigms.

There you go again, equating good and natural.

It seems like even functional decomposition can be unnatural for people even given the commonness of "one big loop" mega-functions as the solution created by a lot of contractors.

Certainly, the problems of mud ball and simple imperative programming styles is that they get harder as you go along.

Indeed, programming methodologies are more or less unnatural ways to program that allow programmers to be more productive and programs to scale in compensation for that unnaturalness.

Consider that mathematicians among the general public, pre-computers, were actually far more rare than functional programmers among programmers. Mathematics is also an unnatural construction. I grew having a great deal of difficulty with handwriting before computers appeared and I learned to love math shortcuts for the writing they saved me. But I also learned most people are more mentally lazy than physically lazy and on average would rather do more work than learn a new thing.

If you view it in terms of psychological development, understanding concrete imperative steps happens before children are capable of understanding abstract concepts like mathematics. The works of Seymour Papert describe how these findings relate to education.

That said, people who connect this to software in a professional context are doing a disservice since hopefully software professionals have progressed in their intellectual development beyond their teens and are capable of abstract mathematical thought.

Concrete imperative steps are extremely mathematical -- even elegantly so -- and most of software verification is predicated on this idea. Just like you can describe physics pretty precisely with simple math, you can do it with computation. The math of PFP is one particular approximation, chosen for its supposed utility as the basis for a language. It is more linguistic math than descriptive math. I also hope people adopt abstract mathematical thought when reasoning about their programs, but that has little to do with the kind of math they choose to model their programs' source code with.
Sure, you can model imperative steps in a variety of ways using mathematics, but for 99% of mathematics there is a far more direct link to pure functions. In how many branches of mathematics do you see a function that is anything but a mapping between sets (or types, or objects in a category). In terms of using the same kind of processes and intuitions, pure functional programming is much closer to mathematics as done by humans than imperative programming. Even theoretical computer science looks this way outside of particular algorithms being studied.

Not that this is some sort of indictment of imperative programming; it's a much better fit for a lot of different situations. But if you're trying to model something in abstract mathematics, pure or pure-ish function programming is likely to be a better solution. For example, look at the various proof assistants like Coq. Many of them are written in functional languages, and if you look at the code you can see why: the manipulation of formulas and proofs fits very well with an immutable, functional approach.

> but for 99% of mathematics there is a far more direct link to pure functions.

But state transitions are pure! Deterministic state machines are nothing more than mathematical functions from one algorithm state to the next, and nondeterministic ones are binary relations! (see this for an overview: http://research.microsoft.com/en-us/um/people/lamport/pubs/s...) Those can then be mathematically manipulated, substituted and what-have-you elegantly and simply. This is how formal verification has been reasoning about programs since the seventies (early on the formalization was a bit more cumbersome, but the concept was as simple).

> For example, look at the various proof assistants like Coq.

For example, look at the specification language (and proof assistant) TLA+. It is far simpler than Coq, just as pure, and requires no more than highschool math.

Coq and other PFP formalizations are built on the core idea of denotational semantics, an approximation which at times only complicates the very simple math of computation, requiring exotic math like category theory.

> the manipulation of formulas and proofs fits very well with an immutable, functional approach.

The manipulation of formulas and proofs fits very well with the imperative approach, only no category theory is required, and core concepts such as simulation/trace-inclusion arise naturally without need for complex embeddings. "PFP math" is what arises after you've decided to use denotational semantics. The most important thing is not to confuse language -- for which PFP has an elegant albeit arcane mathematical formalization -- with the very simple mathematical concepts underlying computation.

PFP is an attempt to apply one specific mathematical formulation to programming languages (other similar, though perhaps less successful attempts are the various process calculi). But it is by no means the natural mathematics of computation. Conflating language with algorithms is a form of deconstructionism: an interesting philosophical concept, but by no means a simple one, and perhaps not the most appropriate one for a science. Short of that, we have the basic, simple core math, and on top of it languages, some espousing various mathematical formalizations, related to the core math in interesting ways, and some less mathematical. But the math of computation isn't created by the language!

> But state transitions are pure! Deterministic state machines are nothing more than (obviously, "pure") functions from one program state to the next, and nondeterministic ones are binary relations!

Okay, yes, but 99% of mathematics isn't deterministic state machines. So again, pure functions can model a lot of things, but deterministic state machines are foreign to the way people think about mathematics in most instances.

> For example, look at the pure specification language (and proof assistant) TLA+. It is far simpler than Coq, just as pure, and requires no more than highschool math.

Okay, that's completely fine as an example. I just mentioned Coq because it's one of the better known ones.

> Coq and other PFP formalizations are built on the core idea of denotational semantics, an approximation which at times only complicates the very simple math of computation, requiring exotic math like category theory.

I'm not sure where CiC (or any of the other recent type theories, including HOTT) requires category theory (which is hardly exotic in 2016). People who are doing meta-mathematics on these systems are interested in categorical models and such, but implementing and using these systems requires no category theory.

Categories are not fundamental objects in these theories; you have to build them just like you do in material set theories, although some parts of that process are easier (especially in HOTT). But that is the reverse of the link that you're claiming.

> The manipulation of formulas and proofs fits very well with the imperative approach, only no category theory is required, and core concepts such as simulation/trace-inclusion arise naturally without need for complex embeddings.

Again, no category theory is required (though I'm still not sure why this is a bad thing?) to develop a prover like Coq or TLA+. If you're bringing simulation and trace-inclusion into this, then you're just saying the stateful, imperative approach is well adapted to working with stateful, imperative systems. I agree, but how exactly does that equate to that approach having any benefit whatsoever for formalizing the rest of mathematics?

> Okay, yes, but 99% of mathematics isn't deterministic state machines. So again, pure functions can model a lot of things, but deterministic state machines are foreign to the way people think about mathematics in most instances.

Sorry, I wasn't clear. 99% of mathematics isn't Schrödinger's equation either, but Schrödinger's equation is still relatively simple math. State machines are simple math, but math isn't state machines. State machines are the concept that underlies computation, and simple math is used to reason about them.

> I just mentioned Coq because it's one of the better known ones.

... and one of the least used ones, at least where software is concerned. There's a reason for that: it's very hard (let alone for engineers) to reason about computer programs in Coq; it's much easier (and done by engineers) to reason about computer programs in TLA+ (or SPIN or B-Method or Alloy).

> which is hardly exotic in 2016

I think it's safe to say that most mathematicians in 2016 -- let alone software engineers -- are pretty unfamiliar with category theory, and have hardly heard of type theory. Engineers, however, already have nearly all the math they need to reason about programs and algorithm in the common mathematical way (they just may not know it, which is why I so obnoxiously bring it up whenever I can, to offset the notion that is way overrepresented here on HN that believes that "PFP is the way to mathematically reason about programs". It is just one way, not even the most common one, and certainly not the easiest one).

> but implementing and using these systems requires no category theory.

Right, but we're talking about foundational principles of computation. Those systems are predicated on denotational semantics, which is a formalization that identifies a computation with the function it computes (yes, some of those systems also have definitional equality, but still, denotational semantics is the core principle), rather than view the computation as built up from functions (in fact, this is precisely what monads do and why they're needed, as the basic denotational semantics fails to capture many important computations). This formalization isn't any better or worse (each could be defined in terms of the other), but it is more complicated, and is unnecessary to mathematically reason about programs. It does require CT concepts like monads to precisely denote certain computations.

> If you're bringing simulation and trace-inclusion into this, then you're just saying the stateful, imperative approach is well adapted to working with stateful, imperative systems.

There are no "imperative systems". Imperative/functional is a feature of the language used to describe a computation, not the computation itself (although, colloquially we say functional/imperative algorithms to refer to those algorithms that commonly arise when using the different linguistic approaches). The algorithm is always a state machine (assuming no textual deconstructionism) -- whether expressed in a language like Haskell or in a language like BASIC -- and that algorithm can be reasoned about with pretty basic math. And I am not talking about a "stateful" approach, but a basic mathematical approach based on state machines (a non-stateful pure functional program also ultimately defines a state machine).

> I agree, but how exactly does that equate to that approach having any benefit whatsoever for formalizing the rest of mathematics?

Oh, I wasn't talking about a new way to formalize the foundation of mathematics (which, I've been told, is the goal of type theory), nor do I think that a new foundation for math is required to mathematically reason about computation (just as it isn't necessary to reason about physics). I just pointed out that algorithms have a very elegant mathematical formulation in "simple" math, which is unrelated to PFP. This formulation serves as the basis for most formal reasoning of computer programs.

That's a great point, thanks! I will look into Papert's work. Are there any specific papers/books you would recommend?
Well the classic here is his book Mindstorms where he talks about how to construct learning environments for children that take into account that bridge between concrete and abstract thought and turn it into an asset instead of a liability.

If you want more on the topic that's not from the hands-on perspective of an educator, you might have to go further back to Piaget's papers, but unfortunately I haven't had the opportunity to dive into these much.

Well, you don't get to decide what people find natural, they do.

And deep below, computers work under a model that's anything but functional (assembly language is basically a big mutable state machine).

There is value in understanding both imperative and functional programming and applying each of them wherever they are the best fit and there are some real world situations where sticking to immutability is a terrible choice (e.g. neural networks).

Programming is about making a piece of hardware do what you want to do. Many people seem to forget that, or treat it as an afterthought. "Oh yeah, it's slow. Hopefully the compiler will do its magic!". Except it does not, because it cannot rewrite your code to match what the hardware wants.

This is why we now live in a world where many programmers don't think there is a problem with running web servers on programming languages that slow down computation by orders of magnitude, in addition to being very cache and memory inefficient. We can scale! Yeah sure, use 50 servers where one could have done it. We would also use Saturn V to launch small individual LEO satellites, you know. Why not? It works!

Taking a functional approach is fine for a lot of tasks, and when you can do it without cryptic or inefficient code, you should do it. But when imperative is easier, use that instead.

You seem to have completely misunderstood my point.

> Well, you don't get to decide what people find natural, they do.

I am not contending that functional/declarative is a more natural way to think; I am saying that there is nothing inherently more natural about imperative programming. Since the latter comes from imitating the machine architecture up in the abstraction hierarchy, we should consider for a moment if this is really desirable.

> And deep below, computers work under a model that's anything but functional (assembly language is basically a big mutable state machine).

This is entirely irrelevant. Computers are highly stateful but the languages we design don't have to be stateful because of this. They have to meet the criterion of easily expressing the mental constructions we designate which I would definitely say is much better handled with statically typed functional programming languages.

> There is value in understanding both imperative and functional programming and applying each of them wherever they are the best fit...

We completely agree on this! Not all problems are best solved with functional languages (e.g., things actually involving the machine).

> there are some real world situations where sticking to immutability is a terrible choice (e.g. neural networks).

I don't see why immutability is a terrible choice for neural networks? Could you elaborate on this? It might be that almost all neural network implementations have been imperative so you have come to accept that as more normal.

Typical neural networks have multiple layers of hundreds of thousands of nodes, which then get updated very frequently during the learning phase.

It would be suicidal for these updates to be immutable (e.g. returning brand new neurons with the updated value instead of updating the neuron that's already in the graph).

> And deep below, computers work under a model that's anything but functional (assembly language is basically a big mutable state machine).

Acutally, deep below a computer is a massive asynchronous electronic circuit. Your "big mutable state machine" is an abstraction, just like any other.

I have a cup of coffee, if I drink a little I don't think a new cup of coffee with less coffee has been created, just that the old one is different. There are some times where imperative thinking seems more natural.
I guess that applies to most programming paradigms. Their metaphors rapidly break down if you want them too.

I also don't send a message to the cup of coffee to reduce the amount of liquid that is in it and pass it a PourIntoMouthStrategy object to act on.

> There are some times where imperative thinking seems more natural

This is certainly true. But there are also times when there is nothing natural about it at all. Consider this scenario:

I give you a cup of coffee. You set it on your desk to cool. Then when you go to take a sip, the cup is empty! You see, I gave you the same cup I was drinking from.

A race condition I guess. At least we didn't try to drink at the same time.

For the record I usually write functional code. Let me put another example instead of coffee. If I have a differential equation, "dx/dt=f(x,t)", I think of the solution as a function "x(t)", but if I try to solve it numerically or even try to reason about it I visualize how "x" changes with time. Is this the reason why numerical code is usually imperative? Maybe it's just because of performance but I really think that sometimes there is a cognitive burden trying to reason fully functionally (I can visualize the parabolic trajectory of a ball but I cannot visualize the 6-dimensional trajectory of an airplane doing a manouver without seeing it "change with time").

I see your point. Learning how to reason effectively in higher and higher levels of abstraction can be daunting; however, it can pay off very well. Ask a mathematician about the set of interesting problems that can be visualized and they will tell you it has measure zero.
I think in a block-time model of the world. The coffee cup now and the coffee cup a minute ago are different things; my memory of the coffee cup a minute ago doesn't suddenly have less coffee in it.
Or a imperative-esque pseudocode called "instructions" or "recipes". Claiming primacy for mathematical modeling is not a priori the answer.
The recipe analogy is certainly very natural, but it does not extend as we get into things like assignments and references, which are a crucial part of Von Neumann languages (which is really the thing I am criticizing).

Tactic languages in theorem provers like Coq are stateful too: you have a proof state and you keep giving instructions for it to change. I wouldn't say that this is an unnatural way to think; for that subtask of the problem, we can view the proof as something with state without any problems.

"Mathematical modeling" sounds too sophisticated to be natural for humans. Let me illustrate what I meant; the kind of naturality I was talking about is this:

sorted(xs): a permutation xs' of xs such that, as we traverse xs' each element is greater than or equal to the previous one.

This is a perfectly fine sorting algorithm. It contains all the information we need to sort a list of comparable things, and it is very unlikely to contain an error; it is the _definition_ of sorting.

There are righteous performance concerns about this but ignoring all performance issues this is still more natural than listing out the steps needed to carry out the task of sorting. I don't think it is possible come up with a simpler recipe-like algorithm for this task.

There are recipe-like tasks in programming: ones that don't involve modeling the world; for those the functional paradigm would not be a good one. But these are rare if you are doing programming the right way.

You are confusing algorithms (Operational Semantics) with definitions (Denotational Semantics). One of the key difference between mathematics and computing is that mathematics is far less concerned with constructing results, and far more concerned with reasoning about the properties of objects. Mathematics sits at a level of abstraction unsuited to physical hardware.

In Haskell, this is evident in the complex and difficult to reason about graph-reduction engine that lays hidden in the runtime. Denotional semantics => Haskell Math yay. Operational Semantics => Haskell's downfall in real-world programmers.

I don't see how either operational or denotational semantics is relevant as I was not talking about semantics of any kind.

I don't think I am confusing anything, you are reacting exactly as I would expect from someone who does not get the point I'm making: we don't have to necessarily program with steps of instructions. If we have a good solver for our language, we can very efficiently execute definitions like my example. I agree that this presents the challenge of efficiently executing such high-level definitions, but this is irrelevant to my point.

> Mathematics sits at a level of abstraction unsuited to physical hardware.

Precisely what I am talking about. Mathematics sits at a level of abstraction convenient for humans not machines.

Have you ever programmed in Prolog? I strongly suggest you try it to get a feeling of why programs are not inherently lists of instructions and how computation can be viewed as deduction.

But in a way, imperative is more natural, because it captures the notion of computation more precisely. Haskell -- like other pure FP languages -- is built around the approximation of denotational semantics, which does have a bit of a mismatch with computation (not to say it isn't extremely useful much of the time). Anyway, mathematical thinking about programs didn't start with PFP, nor is PFP the most common way of formalizing programs. See:

http://research.microsoft.com/en-us/um/people/lamport/pubs/s...

I believe that the best way to get better programs is to teach programmers how to think better. Thinking is not the ability to manipulate language; it’s the ability to manipulate concepts. Computer science should be about concepts, not languages. But how does one teach concepts without getting distracted by the language in which those concepts are expressed? My answer is to use the same language as every other branch of science and engineering—namely, mathematics.

It makes me sad that some PFP enthusiasts enjoy the mathematical aspects of it -- as they should -- yet are unfamiliar with the more "classical" mathematical thinking. I think it's important to grasp the more precise mathematics first, and only then choose which approximations you'd make in the language of your choice. Otherwise you get what Lamport calls "Whorfian syndrome — the confusion of language with reality".

Thanks for pointing this out!

Dijkstra, in his "on the cruelty of really teaching computing science" makes a very similar point that people should learn how to reason about programs before learning to program.

This is why dependent types are a great framework to program in. If a program is worth writing, it should at least contain everything the programmer thinks about the program, including the reasoning of why it is the program he wants to write!

Right. In computational semantics there are two schools. The "school of Dijkstra" (now championed most vocally by Lamport) -- which has largely taken hold in the field of formal verification -- and the "school of Milner" (Backus?) -- which has largely taken hold in the field of programming language theory. The former reasons in concepts and abstract structures (computations, Kripke structures), and the latter reasons in languages ("calculi").

The interesting philosophical question is this: can programs be said to exist as concepts independent of the language in which they are coded (in which case the language is an artificial, useful construct) or not (in which case the concept is an artificial useful construct)?

Whatever your viewpoint, the "conceptual" state machine math is a lot simpler than the linguistic math offered by PFP.

> Haskell -- like other pure FP languages -- is built around > the approximation of denotational semantics,

Interesting, do you have any references for this? I thought that the primary reason for purity was to enable equational reasoning, but I have no sources for this. Also, AFAIK, there are no formal semantics for Haskell?

I think the first few chapters here explain this nicely: http://www.paultaylor.eu/~pt/stable/prot.pdf

Don't know about a formal spec. I'm not a Haskell developer.