"The avoidance of side-effects is one of the key objectives of functional programming. Advocates of functional programming claim a number of benefits for the absence of side-effects including a greater ease of reading and reasoning about the code and an increased ability to execute parts of a computation in parallel."
You know I'm sympathetic to this line of thinking, but I wished he would have actually used this VM example to back up these statements.
Why is the version without mutable state better? That's not explained at all. I can think of some reasons why it would be worse -- e.g. more memory allocations. For this VM, its semantics are inherently serial, so the concurrency argument doesn't apply.
Well, the second version here is better for the following reasons:
- 9 less lines of code.
- Easier to see that it's correct. Partly because it's less lines, but partly because those lines are more meaningful and less bland. It would be hard to see a typo in the repetitive inner loop of the first version.
- Two obvious independent pieces of code instead of one big piece of code. That means if you're reading it, you know how to go about understanding it; understand each piece in turn. Somewhat trivial when there are 25 lines, but serious if there are 200 lines.
- Easier to test and debug. At a REPL, it's trivial to see what the stack is going to look like five hops into the functional version; you just apply the processing function five times. To do the same to the imperative version, you need to either write some debug logic into the middle of the function or set a breakpoint and hit 'continue' in your debugger while you count on your fingers.
Those reasons are typical consequences of writing your code as a collection of data and pure functions.
Also, the semantics are not completely inherently serial for most possible stacks. The sequence of operations forms a tree of computation, and the branches of the tree could be computed in parallel. If you actually wanted to do that (say, if the operations took a long time individually) then the way you would go about it is to construct the tree explicitly up front, take this functional version, replace fold with a version of fold that knows how to fold over trees in parallel, and fold it over the tree. Ta-da -- it would just work.
- Wrap the stack program in a transaction and simply discard the results to roll back the transaction. In an imperative program it is error prone to roll back a complicated operation—the most innocent looking function might mutate external state.
My read on this is not that he is claiming those things, but that he is examining those claims to see if they are true.
To be honest, not getting another breathless reiteration of the Functional Programming Dogma is a breath of fresh air. I actually like FP in the general sense, think interesting things are happening there, and am keeping an eye on it (as well as using it in selected places in my job where it makes sense, so I've actually put some cash down on the thing), but I'm really tired of people who have just barely learned enough FP to implement something of, say, this exact level of complexity making wild boasts about how good it is. I suppose it's a specific case of the general problem of seeing someone argue something you agree with, but badly.
This particular "integer stack VM implementation" is so trivial that you can't make arguments based on it. Both the imperative and functional implementations are too tiny to make pronouncements.
The most, imho, accessible discussion of this stuff is Dybvig's 3 implementations of scheme. The first 75 pages or so are just a great way of using a functional style of thinking to implement a functional VM. http://www.cs.unm.edu/~williams/cs491/three-imp.pdf
It's long and dense, but there's is a valuable point view there, and scheme sidesteps the whole strong typing set of issues. You get a complete, optimized scheme interepreter in , what?, maybe 150 lines of code? seminal paper for an autodidact.
Good question. The thing I most loved was the progressive refinement of the technique. I love seeing things transformed from rough to fine and seeing the rationale for each step.
It's neat to see how much more expressive F# manages to be compared to C#. One of his examples (passing an operator as an argument to a function) would probably take dozens or hundreds of lines to express in C# because it's just not something that's built into the language.
That's not an operator, that's a lambda. The end result is the same, but semantically it's very different. Passing around an operator means you don't need to know the types involved - you can have a function that operates on any two values and applies an operator to them. That's not something you can do with lambdas in C#.
No, there's no difference whatsoever. .NET has no concept of "operators" that are different from functions.
F# is statically typed, just like C#, and the only reason you can pass + in this case is because the type of the stack is Stack<int>, so the compiler can statically infer that "+" means "the + operator on ints." At every usage, + has to refer to a specific function, or it wouldn't compile. For example, if it was a Stack<object> that happened to contain ints, it wouldn't compile.
(You could use .NET's "dynamic" type to make it dynamically resolve which function at runtime, but you could use it equally well in the C# example with a lambda.)
Non-overloadable operators (short-circuiting operators (|| and &&), ternaries and its ilk, field access and (of course) the assignment operator) are probably compiled straight to bytecode, but they're pretty special and (especially in a strict language) need special runtime support anyway[0].
To sum up: mquander is completely right, the CLR does not know anything about operators, language compilers will take whatever operator they have at a high level and compile them to method calls (usually). For more details, see http://www.amazon.com/dp/0735627045/
Interesting. However, operators and functions present very differently when writing c# code.
It's easy to put a lambda or a named function in a variable, pass it to a function, etc. However, if you want to do that to an operator such as "+", the first thing to do is to wrap it in a lambda - e.g.
// invalid
// Func<int, int, int> plus = + ;
// works in a wrapper fn
Func<int, int, int> plusFn = (a, b) => a + b;
Related, if you have a function which is generic for some type T, you can call functions of type Func<T> on the values in it, but you can't use operators like + - c# just isn't generic on operators, though it can be on functions.
public T ApplySomeFunc<T>(Func<T, T> func, T value)
{
// this works
return func(value);
// this does not
//return value + value;
}
I know that the language has no way to specify that the type T has a "+" operator so that's not going to work; but this catches people out regularly, when they try and fail to make math functions that are generic over all numeric types. However, you can do something similar with the "dynamic" type, at the cost or run-time-dispatch.
> Interesting. However, operators and functions present very differently when writing c# code.
Aye, but that's more of a limitation of C#'s syntax
> However, if you want to do that to an operator such as "+", the first thing to do is to wrap it in a lambda - e.g.
That's pretty much what a section (the parens around the operators) does in F# or Haskell.
> I know that the language has no way to specify that the type T has a "+" operator so that's not going to work
Well it's not so much that "operators and functions present very differently when writing C# code" but that "methods and functions present very differently when writing C# code" and that operators are (static) methods more than functions in C#.
Also that C# has no numeric tower[0] so there's indeed no way to say a type is a generic number (whether that number's an int, a double or a decimal). If there was a root `Number` type you could write something along the lines of:
public T AddNumbers<T> where T:Number(T value)
{
// works because all numbers have a `+`
return value + value;
}
which is exactly what you write in Haskell:
addNumbers :: (Num a) => a -> a -> a
addNumbers a b = a + b
[0] Haskell has a pretty complete/complex one, starting from Num[1] which implements only a few basic operations: (+), (-), (*), negation and conversion from integer.
> That's not an operator, that's a lambda. The end result is the same, but semantically it's very different.
It's not, actually. In most functional languages — including F# — operators are just a special case of functions. Haskell will let you both use operators as (prefix) functions (via sections):
Prelude> (/) 42 2
21.0
or use functions as (infix, binary) operators:
Prelude> even `filter` [0..20]
[0,2,4,6,8,10,12,14,16,18,20]
and operators are defined exactly the same way you'd define functions:
Prelude> let (><) a b = a * b in 3 >< 4
12
although you can define them infix if you prefer:
Prelude> let a >< b = a * b in 3 >< 4
12
then again, you can also define functions using infix syntax:
Prelude> let a `add` b = a + b in add 3 4
7
You can also partially apply operators (again, via sections):
Prelude> :t (1 +)
(1 +) :: Num a => a -> a
in roughly the same way you'd partially apply functions:
Prelude> :t map (+ 1)
map (+ 1) :: Num b => [b] -> [b]
> Passing around an operator means you don't need to know the types involved
Uh? You need to know them in exactly the same way you do for functions.
> you can have a function that operates on any two values and applies an operator to them.
No, you most definitely can't, both operators and functions are typed. They work the exact same way but for the syntactic difference that operators are infix and may have precedence junk on top. But that's parsing, aside from that, a functional language will treat functions and operators the same way (outside of the odd builtin)
You know I'm sympathetic to this line of thinking, but I wished he would have actually used this VM example to back up these statements.
Why is the version without mutable state better? That's not explained at all. I can think of some reasons why it would be worse -- e.g. more memory allocations. For this VM, its semantics are inherently serial, so the concurrency argument doesn't apply.