Functional Programming in C++ (2013)
web.archive.org
web.archive.org
That said, the problem of not understanding possible code states does not seem to be purely a result of the internal software structure, but how it interacts with outside components, including the OS (if any).
There is a combinatorial explosion of the state space as a program's dependencies increase. As such, the most practical way to deal with this is crash-only software whereby components do not perform complicated error recovery, but are simply fenced and restarted to known good states with persistence used to ensure that the component can return to its prior point of operation quickly.
FP is useful for writing software like this, but not necessarily pure FP or even a single paradigm.
https://news.ycombinator.com/newsguidelines.html
(Submitted title was 'John Carmack: Why functional programming is the future'.)
object Foo {
var x: Int = 0
def bar(y: Int) = x + y
}
And trying to figure out what bar will return at any given point. public class MyBigClass() {
private int a;
private int b;
//hundreds of lines of code
private int calculateSomething() {
//do something
a = a+1;
return b*b;
}
//hundreds lines of code
private int calculateSomethingElse() {
int c = calculateSomething();
return a+c;
}
//hundreds of lines of code
public returnSomething() {
return calculateSomethingElse();
}
}
In this example. It's hard because you have to remember constantly which functions are changing your classes before you use them, and if you haven't written the functions, it's very easy to introduce a bug by involuntarily modify the states using the function calculateSomething, just wanting its output and not the modification of a.The guy who is not using encapsulation at all this year will be making similar mistakes (massively long bodies, proliferation of unnecessary entities, etc.) if he switches to functional programming next year. It's not a panacea.
I'm afraid this doesn't demonstrate that any tiny bit of statefulness is "exhausting."
The 'world' is carried on the back of one parameter, right? So if I want to run a function taking two parameters in a particular 'world', which parameter is the Monad? If both of them are, what if they disagree which world is used?
Perhaps I haven't reached the fabled epiphany of monads, but they seem to be just implicit state attached to a single value, and only a single value. I'm not sure what this implicit stuff is better than a system where you are explicitly passing the 'world' that each function needs.
Why is
roll(dieSides: Random<int>) -> int
Clearer than
roll(random: Random, int dieSides) -> int
I get the passing of a world out of a function with a single value
getNumber() -> IO<int>
or using the extra implicit state to represent other possible values, like
Maybe<int>
but, using it to carry around state? That seems to me to be unnecessarily implicit.
What am I missing?
do
(x, g2) <- roll g1
(y, g3) <- roll g2
(z, g4) <- roll g3
it's convenient to have plumbing that hides all those single-use g values.An example of a monad which has really nothing necessarily to do with mutable state are Promises in JavaScript. The container, of course, is a Promise, and the two functions I described are `resolve` and `then`.
In particular the issue I was addressing was the encapsulation of an arbitrary state with a single typed value. It's been remarked elsewhere that 'mixing' monads is a problem: I guess that's always been the thing I've felt haven't made them work replicating as a coding pattern. I tend to prefer explicit over implicit state.
I guess it depends on your definitions, but monadic state is just as explicit as any other.
myMonadicFunction :: String -> State Int String
myMonadicFunction string = do
currentVal <- get
put (currentVal + length string)
return (reverse string)
It's just as clear as what you'd have in an imperative programming language.I'm convinced the main reason monads aren't common in other languages is that most languages just don't have strong enough type systems to support them explicitly. Implicitly they can be found here or there, such as with promises, or just the fact that the semantics of many imperative languages can be viewed as a monad. But of course, probably the biggest reason is that people who are used to mutable state don't see the need. It's one of those things whose value becomes apparent with use.
What's to stop you passing a struct or object with the two monadic functions attached, or ignore the function requirement (for the case of state) and pass the struct with the state and result in it? The benefit being that you can package up more than just one value in that (immutable) structure along with the 'specialness'. I don't see why that is less typesafe.
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
And it's up to the particular instance to determine how those functions are implemented. For example, instance Monad Maybe where
return :: a -> Maybe a
return x = Just x
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
maybe >>= f = case maybe of
Nothing -> Nothing
Just x -> f x
The definition of the State type, and its corresponding Monad instance, is a little more involved (I suggest this blog post for an explanation [1]): newtype State s a = State { runState :: s -> (a, s) }
instance Monad (State a) where
return x = State $ \s -> (x, s)
m >>= k = State $ \s -> let (a, s') = runState m s
in runState (k a) s'
I'm honestly not quite sure what you mean by the rest of it (for example, "ignoring the function requirement", "specialness" etc). But the bottom line is that you can do any typesafe thing you want to, within the rules of how the language works.[1]: http://brandon.si/code/the-state-monad-a-tutorial-for-the-co...
Sorry, I wasn't talking about Haskell. Following the OP I was talking about the merits of using these concepts or structures in other languages (C++, as per the OP, in particular). Of course, in a language with strong support and syntax for a particular pattern, you should rely on that pattern. But beyond that, it can be useful to use a pattern in other languages. Carmack is talking about writing C++ in a more functional style. I've written OO plain C, for example, when that pattern has made sense, and much of my C++ is heavily influenced by my experience with Scheme.
My point was that I don't get the emphasis on Monads, as a concept, for the range of things it gets emphasised for. In a language which has core syntactic and semantic support, and there are few alternatives, then of course it has to be used widely. But that's a different argument to saying it is an intrinsically good solution for those cases. I might be missing something, but as an architectural pattern in general it doesn't seem appealing. Not enough to reimplement in another language.
That's the context to my comment on typesafety. You said that other languages aren't typesafe enough. I was suggesting that, implementing a monad as a structure or an object would be typesafe, so I couldn't see what was insufficient.
I get the sense that there are people who think Monads are a fundamentally powerful pattern for structuring software. And I don't get that. And I'd like to, because if I'm missing something, it could mean I improve my code architecture.
Monad's qualify as a 'that's an interesting way to get around its self-imposed restrictions' moment when learning Haskell (which I've not used with any seriousness) for me. So I don't get the fuss. I'm not saying that I don't see the logic of those restrictions, nor that I don't see why you'd want to voluntarily code with similar restrictions in other languages. More than, if you're going to write something that has to work around those restrictions in another language, I've no idea why you'd necessarily chose Monads.
Or, put another way, what use are Monads in non Haskell code?
One of the tricks is the use of monads in Haskell. Monads are really just a small set of functions that operate in a certain way (that "satisfies the monadic laws") on a type.
What that gives you is (among many other things) a way to carry an implicit "world" around with you without needing to refer to it all the time.
One use for such a "world" would be a seed for a random number generator so different parts of your program can use their own, isolated streams of random numbers (nice for determinism, testing, debugging, etc), even if they execute asynchronously because Haskell is a "lazy" language where code doesn't necessarily get evaluated unless it really needs to.
Another use is for the "world" to actually represent the real world -- all of it -- so the I/O interactions happen in the right order. That's very important in a language where the evaluation order is so "loose" as it is in Haskell.
There are other tricks as well. One of them is to have lots of small transformation functions that don't need to understand all of the world, just their little bit of it. You create the big world transformations by composing the smaller transformations. The data structures you use are slightly different: you don't want to modify your data structures, you want to create new ones all the time, i.e. inserting an object into a tree shouldn't modify the tree but return a completely new tree with the new object in it WHILE KEEPING THE OLD TREE AROUND UNCHANGED. This doesn't have to be as expensive as it sounds, in fact, it can be quite cheap. Data structures where you can do this cheaply are called persistent data structures and they work really well with a functional programming style.
(They don't have to implemented purely functionally internally as long as they present a pure interface.)
https://en.wikipedia.org/wiki/Persistent_data_structure
Persistent data structures can also be confluent, which means that they use sharing internally if two versions of a data structure end up being logically the same. Tarjan and Sleator have some really nice papers on that -- well worth reading, even if you don't care one bit about functional programming.
if (std::find(container.begin(), container.end(), element) != container.end())
My fingers are now cramping and my eyes are crossing trying to read that. It beggars belief that overloads don't exist which take an entire range which is all I want to do 99% of the time.A 3-4 line template will will give you a terse boolean contains() method, but it won't give you any big-O guarantees, and having such a thing would encourage very inefficient patterns (like calling contains() and then doing a find() if it's true, which effectively does the lookup twice). Why introduce a weak abstraction? If you're forced to write it yourself you're more likely to understand the implications.
Alex Stepanov, who wrote the original STL, is very much a believer in fundamental indivisible building blocks like iterators and algorithms and careful API design. To grok more about why things are the way they are, I recommend watching his "Efficient Programming with Components" lectures[0]. Over the course he builds up, in excruciating detail, an implementation of merge sort, reasoning about, and discussing with his students, the implications of almost every line of code.
And yes, Stepanov complains about the lack of terseness and expressiveness in C++ as well.
[0] https://www.youtube.com/playlist?list=PLHxtyCq_WDLXryyw91lah...
auto e = end(container);
if (e == std::find(begin(container), e, element)) { ... }
...but yeah, it sucks. If I were doing this though, i'd to be encapsulating things in to a higher level abstraction. Seeing std::find in more than 1 place in a file is a red light for me.I really like functional programming, especially in Standard ML and Haskell. I didn't learn ML until I'd been programming for about 15 years.
Edit: beadder grammer.
Using a function style or a higher-level language sometimes makes it easier to modify the program (and hence use a better algorithm).
I remember people swearing by assembly language back in the 80's, thinking C would make their programs slow and bloated. In reality, it was usually the other way around because C code is much more malleable than assembly. You might want to give FP a second look.
https://en.wikipedia.org/wiki/C%2B%2B_AMP
However, my best algorithms are stylistically equivalent to a map-reduce with combiners entirely in GPU-space. The map tasks themselves carry all the scope I need. They are independent units of work executed by independent warps (synchronized groups of 32 GPU threads) whose results need to be reduced to floating point numbers. That reduction is done with 64-bit fixed point atomic ops (my combiners) because:
1) This insures the sum is deterministic
2) Ever since Kepler (GK104), it's much faster than reduction buffers and uses a fraction of the memory
3) It's possible because I can precompute the expected dynamic range and adjust the fixed point exponent accordingly
Now given that, do you see a functional programming equivalent that significantly improves on this design? I haven't yet.
Who cares if you implement the combining with atomic ops or by sending data to a process that does the combining or whether you stuff the data in a buffer and then reduce it afterwards?
(Of course you might care for performance reasons -- but conceptually it's the same thing.)
1) "Who cares" is not the sort of thing you want to say to someone who cares about performance because:
2) Sending the data to buffers for subsequent reduction was the first implementation (2009). But it was a memory hog and a 5% or so slowdown to perform the reduction subsequently rather than concurrently with Atomic Ops and 64-bit Fixed Point (2012).
But I guess what we're arriving at is that this is essentially a functional design using imperative code? I can live with that.
Yep. That's precisely what it is.
[1] At least the Haskell/ML kind.
Hackers, and I'll assume all those vaguely tracing their lineage from MIT, have never been associated with FP. There has always been a fascination with Lisp, but that's the extent of it. In practice, most of their software legacy has been in firmly imperative and procedural languages, but with touches of a Lisp here and there.
It's precisely CS and academia where functional programming has been thriving. John Backus' research on function-level programming, Robin Milner on ML, David Turner on Miranda from which Haskell is almost a direct successor, etc.
So on the whole, I have the exact opposite impression that you do.
I think this preference has more to do with the individual's past experiences and future goals than it does with his/her level of formal CS education.
[1] Although even this varies greatly from school to school, the school where I got my MS used OCaml in the undergrad algorithms course for awhile. My undergrad algorithms course used C, so again no OOP.
If you learn type theory and semantics you also learn functional programming (and will probably like it, even if you don't use it much in practice).
If all the world is a Java VM to you, then... well, you might have a different view of the world.
Functional languages seems to be one of the hardest things for people without a CS background to pickup in my experience.