1,132 karma · joined January 2, 2012
In short, no - as an example, when an element is added to an immutable map, a big part of the newly produced map is structurally shared with the old one (check out [0].)
As for the ordering, you could have a list of geometry -> geometry functions. You could then take the initial geometry and apply the list of functions in order, or a permutation of it, or drop some functions, or apply some others multiple times, and so on.
But anyway, it's just an idea - thinking in terms of atomic geometry transformations instead of alternations may or may not be easier in more complex scenarios, I don't know :)
[0] http://hypirion.com/musings/understanding-persistent-vector-...
What I meant is that it might be simpler to just have pure functions operating on immutable data structures, which you could then compose in whichever you want. I.e. you could have a function collapse-vertices that takes some geometry and produces some new geometry (without altering the initial one!), which you could then compose with some other pure function, like produce-random-vertices, and so on, much like a Unix pipe.
A free monad is just a formalization of the command/interpreter pattern in a purely functional context (i.e. most useful in languages like Haskell or Clojure).
Also, what are the advantages of this approach over simple function composition operating on immutable data structures/geometries?
But the general pattern applies to many things. One other example that immediately springs to mind is e.g. registering a callback while needing the ability to unregister the callback from within itself, using some sort of id returned from the registering function.
mfix $ \threadId -> forkIO $ do
-- computation in forked thread
forkIO creates a new thread and returns its thread id. However, in order to access the returned thread id inside the forked computation, normally one would have to store it in a variable and then read from it inside the thread.mfix, by nature of its laziness, captures the return value of the function passed to it, and passes it to said function.
Row polymorphism is indeed very useful. Purescript (a Haskell descendant compiling to js) has it and I wish Haskell did too.
> The point of using OO in a giant complex program is the isolation you get, so that a team of 1000 people can develop the app in a reasonable way.
> Not everything is easily isolated: Sometimes you need a control over here to poke into the state of a control over there, and you don't want that information in some "god object" at the top level; you want a direct connection and mutability.
The above two statements directly contradict each other. Yes, OOP preaches isolation and programming against abstract interfaces, in theory. In practice, more often than not one is tempted to do something the easy way and "poke into the state" of another object. This has disastrous consequences: you never know what will happen where when you call a method. Everything is fair game. It's like changing the lock of the basement door in a skyscraper somewhere only to have it fall because the steal beams depended on this exact lock to hold some state, since it was "faster that way". It's insane.
I've seen so many OOP clusterfucks that I seriously can't believe that you haven't in your much longer career.
Sometimes FP is not a good idea - mostly when performance, memory or deterministic runtime behaviour is a must. But even there languages like Rust show that it may be possible to have one's cake and eat it too.
Other than that, I'd be genuinely interested in hearing what kind of problems you think FP wouldn't work well for.
> Which is why it's so popular for solving complex problems?
It is, actually. I've worked on a very complex codebase in Haskell at my last job (a pathfinding and ticket price calculation backend with ridiculous specifications full of special cases and exceptions that changed almost every day) and I was able to be productive almost from day one. We could refactor with almost absolute confidence because the language is pure and type safe, and we had to because the specs changed so quickly.
Facebook's spam filter and Jane Street's OCaml stack come to mind as other examples.
There aren't as many applications written in FP languages simply because these languages aren't that popular. But popularity isn't a metric of quality.
> If you're putting more cognitive load on the developer, then you're just changing the problems, not preventing them.
I absolutely find the opposite to be the case. Haskell reduces the cognitive load on the programmer, because it is pure (i.e. no need to keeping track of dozens of interactions at once) and type safe (i.e. a function marked as pure can't call a function with IO).
Not to mention things like an actual working software transactional memory implementation that makes concurrent programming as easy as writing a single threaded program. No other language, with the notable exception of maybe Clojure, has a practical STM library. But anyway, STM is just a bonus.
In short, I would say that FP actually delivers what OOP set out to but couldn't. Many FP ideas that were almost unheard of just a couple years ago, like immutable data, pure functions, composition over inheritance, non-nullable types, algebraic data types and so on are slowly but surely taking hold in the mainstream. Pure FP languages just take it to the next level :)
Well, not quite; take a look at the Elm architecture [0]. It's basically what Redux gives you, but more explicit.
The idea is, that yes, while you have a god object in the top-most component, child components don't know anything about it; they operate on their piece of state completely independently. This makes UI elements perfectly composable and reusable, since in the end, a UI element is just a pure function and nothing more.
Not to mention other nice side-effects (sorry :)) of pure UI architectures, like time-travelling debugging, sane logging and so on.
> FP is great for problems up to a certain complexity. After that it falls apart.
I disagree completely. FP, and moreover pure FP, is especially well suited for complex problems. Pure functions are testable (if you haven't heard of QuickCheck / generative testing, you've missed out) and as I said above, composable.
If anything, I've never seen a big OO program made out of truly reusable components. Because classes and methods are often impure, before calling a method one must make sure that the correct environment is set up in the right way (if you've ever forgotten calling initSubsystem() before subsytemDoSomething() you know what I mean).
One can rip out a pure function from one codebase and reuse it, as-is, no modification necessary, in another one without any problems. How often does this happen in a big OO project?
> Is "instance" adding an interface to TileMap/HexMap so that either can be passed in as a parameter that requires a RectIterable? Can other interfaces be added that way to TileMap/HexMap?
Yes, and yes. In Java, when you have a class like this:
class A implements B, C {
...
}
in some library, you can't add D to the list of implemented interfaces.In Haskell, you can:
instance D A where
...
Haskell assumes an "open world" - i.e. everybody can add instances to a data type even after it has been defined already.I can't give you a complete overview of the differences between ad-hoc and parametric polymorphism, but there are plenty of resources online if you are interested.
> In my editor, can I say something like myTileMap.iterateRect after doing this, or do I need to know that iterateRect exists and remember exactly what it's called?
I can only speak for Haskell here. It's a static language, so things like autocompletion, show type at cursor, jump to definition etc are not a problem. The tooling could be a whole lot better, no doubt, but it's not bad. The REPL helps a lot, too.
Basic example, puring cream into a cup of coffee. At first low entropy, low complexity (cream at top, coffee at bottom). Then higher entropy, high complexity (cream and coffee mix according to laws of physics, different concentrations at different locations in the cup, streams and such). At last, highest entropy but again low complexity (everything is homogenous).
None of the things you listed are particularly hard in an FP context. Classes/traits and parametric polymorphism will allow you to do these things just fine (Haskell code):
class RectIterable map where
iterateRect :: map -> (Point -> a) -> Rectangle -> [a]
// define map data structures
data TileMap = ...
data HexMap = ...
// make them instances of the RectIterable class
instance RectIterable TileMap where
iterateRect = ...
instance RectIterable HexMap where
iterateRect = ...
> Object oriented programming ... is good for UII don't know, but React begs to differ. A pure function (state -> UI) is basically one of the best UI programming paradigms we've come up with so far, the other probably being FRP.
Regardless, you can't maintain state with closures in a pure language. You could close over some local "variable", but that variable would be immutable. In an impure language that would work, but then you're back to managing global state, even if that state is not defined at the top level (i.e. global in the sense that different closures could still have access to the same piece of state, concurrently).
[0] https://news.ycombinator.com/item?id=13965917
EDIT: just to clarify, monads manage state purely. I.e, instead of writing:
function statefulFunction(state) {
(a, state1) = f(state);
(b, state2) = g(a, state1);
(c, state3) = h(b, state2);
return (c, state3);
}
you can write: statefulFunction = do
a <- f
b <- g a
c <- h b
return c
But the resulting function is still pure, i.e. (state) => (c, state). data Address = Address
{ city :: String
, street :: String
}
data Person = Person
{ name :: String
, address :: Address
}
My general impression of recent-ish OOP developments is that people have started preferring composition over inheritance anyway, for good reasons. I guess I should've framed by question more like "the merits of inheritance over composition". bind(StatefulFunction<a>, a => StatefulFunction<b>) => StatefulFunction<b>
flatMap(List<a>, a => List<b>) => List<b>
Look at the structure. Don't pay attention to what List or StatefulFunction actually are, that's not important here. The thing to focus on is that if you can provide this kind of general function for something, then that something is probably a monad.I.e. a function that takes in Something<a> and a function from a to Something<b> and produces Something<b>.
So why is this pattern so useful that everybody freaks out about it?
Well, that syntax sugar I showed you at the end, let's look at it again:
do
a <- f
b <- g a
h b
The general idea here is that "a <- f" is translated into bind(f, a => ...). The whole block would turn into bind(f, a => bind(g(a), b => h(b)));
Bear with me. Now, let's look at one more example: bind(Optional<a>, a => Optional<b>) => Optional<b>
Optional<a> is just a data type that maybe contains an <a>. Think of it as a safe replacement for null. So, bind for Optional looks at the first argumenta and passes it on to the function and returns its result, if the Optional is not empty. If it is, bind also returns an empty Optional.Now let' say we have a bunch of Optional producing functions:
getUser(userId) => Optional<User> // user may not exist
getAddress(User) => Optional<Address> // user may not have entered an address
getStreet(Address) => Optional<Street> // address may be weird
Now ok, what would we do in a language without monads? user = getUser(userId);
if (user.hasValue()) {
address = getAddress(user.getValue());
if (address.hasValue()) {
street = getStreet(address);
// do something
}
}
Well, that's not great. But because Optional is a monad and we have that nifty bind sugar, that's how that code would look like in Haskell: do
user <- getUser userId
address <- getAddress user
street <- getStreet address
Much better.More generally, it turns out monads are literally everywhere. The guy that talked about CPS transformed programs being monads is right - that's the continuation monad.
Let's look at that. If you know node.js, you know that its API is basically asynchronous:
fs.open(path, callback)
fs.read(fd, ..., callback)
fs.close(fs, callback)
Now, let's read some file in node: fs.open(path, function(fd) {
fs.read(fd, ..., function(result) {
// do something with result
fs.close(fs, function() {
// file closed
}
}
}
Well, that's ridiculous. But becase we have the continuation monad, we can write exactly the same thing in Haskell: do
fd <- open path
result <- read fd
// do something
close fd
So why are monads so useful? Because they are so general. They can be used to abstract over many many things that exhibit the basic pattern (S<a>, a => S<b>) => S<b>. Not to mention that when you write a function to generally work with monads, it will work for ANY monad. List, State, Maybe, Cont - doesn't matter.There's not much more to it than recognizing that pattern. That's it. Once you see that that's literally it you'll start seeing monads everywhere :)
But I actually wasn't asking about pure vs imperative, but rather FP vs OOP. I haven't heard of a pure OOP language, but in that context I think those properties could be seen as more or less orthogonal.
Let's say you need a function that maintains internal state. Now, in non-pure languages, you could that by simply referring to some global variable, but let's agree that's bad. So how can you model internal state purely?
Simple, your function takes the current state and produces a new state, along with its result. So, a pure function like:
f(String) => Int
turns into the stateful function: f(String) => (State => [Int, State])
I.e. a function that returns a function that takes in a state and produces a result and a new state.Cool, but now let's say we have another function g:
g(Int) => (State => [Double, State])
Let's now say we want to compose those two functions: let result = g(f("5"));
However, this won't work, becase f produces (State => [Int, State]) but g expects Int.So, here's bind for the State monad (implementation is not important):
bind(State => [a, State], a => (State => [b, State])) => (State => [b, State])
If we define a "type alias" (just a synonym, so we don't have to type that much) for a stateful function: type StatefulFunction<a> = State => [a, State]
and then replace the above bind definition with it: bind(StatefulFunction<a>, a => StatefulFunction<b>) => StatefulFunction<b>
you can see that is has the same structure as flatMap! flatMap(List<a>, a => List<b>) => List<b>
With the type alias, f and g would then look like this: f(String) => StatefulFunction<Int>
f(Int) => StatefulFunction<Double>
So now, here's how you compose f and g: bind(f("5"), a => g(a))
In Haskell, there's syntax sugar for bind, so the above turns into do
a <- f "5"
g a
which is very similar to a conventional language: {
a = f(5);
g(a);
}
EDIT: forgot result type in bind and flatMap.If the universe could be simulated, you could, in theory, run that simulation on a (very large) piece of paper by hand. Running the simulation would then be equivalent to calculating some numbers and writing them down.
Basically, if I wrote down the correct sequence of numbers, I could simulate you. But is calculating those numbers actually necessary? Is it enough to just have the result of that computation without performing the actual steps to obtain it? If yes, then a random number generator would be all that is needed to simulate our (and infinitely many other) universes.
And if that's the case, maybe it's not such a big leap to imagine that the mere existence of the physical laws as an abstract concept is enough to give rise to a simulation?
Care to elaborate a bit more?
I understand that when you see for(...) { b[i] = f(a[i]); } then you instinctively know that it's a loop that maps over some sequence, and so on. But let's say you have a 50-100kloc codebase with a 100 files in it. In what unique to Go way does Go help you understand the big picture/coarse architecture of such a project?
> But you don't get that effect by ramming it down other people's throats, you get that effect by showing it in practice.
I disagree. I've seen many C programmers who think that C is the be all end all of programming languages. Those people will not be convinced by showing them another technology that is better in practice. The fact that it's actually unbelievably hard to write correct software in C is somehow a point of pride for many people, though I speculate that few of them can actually follow through.
Of course there will always be logic bugs in software, even with formal systems and whatnot (i.e. bugs in the specifications). But memory bugs and the resulting security exploits could in many cases be a thing of the past already.
Many industries have similar regulations, i.e. seatbelts in cars. If somebody dies because of a faulty or non-existing seatbelt, there will be consequences. If somebody dies because they were talking on the phone and the car didn't prevent it, well, you can't control everything.
Is this a buffer overflow? Why was this written in C? No good answer - pay up.
There are times when C is unavoidable, unfortunately, but when somebody chooses a technology regardless of better, safer alternatives, "just because", they should be made liable, absolutely.
This is like hiring somebody to build a bridge, it falling due to incompetence killing hundreds of people and then having nobody suffer any consequences. "Eh, bridges fall, what can you do."
Make people pay for buffer overflows and C is gone tomorrow.
Unless you believe in absolute morality or the like, there's no such thing as an objective measure. A goal can only be optimal to an agent.
In your example, the fact that we pursue science and can destroy ants doesn't mean that their goal is "objectively less optimal". Their goal is absolutely optimal to them, though they can't reach it if it collides with ours.
Some goes for a superintelligent AI.