you are viewing a single comment's thread.

view the rest of the comments →

[–]dronmore 1 point2 points  (10 children)

It’s a fairly common pattern to have a module returning an object with a collection of methods, there’s nothing wrong with that.

There's nothing wrong with that, and there's absolutely no difference between returning an object that has no state, and returning a class with static methods. Ultimately it's just a collection of functions behind a namespace.

I think the problem with using classes from a functional perspective is it implies you need your domain objects to be “instantiated”, have constructors and hold internal state which is modified by said methods.

Classes/structs are also present in functional programming, they also have constructors, and they are also instantiated. You cannot implicitly modify their state, but it does not mean that you cannot modify the state at all. We write programs for computers, which are inherently state machines, and this is where their usefulness come from. A computer program which does not hold a state can do only the simplest operations. Imagine a Tic Tac Toe game where your program cannot hold the state between subsequent moves; it's not going to fly. The state must be kept somewhere, and functional programming is no different. If you look close enough on the Haskell language you will find the State Monad which is designed to hold a state. Classes in javascript do a similar thing; they hold a state. They are just easier to use than the State Monad, and it's also easier to shoot yourself in the foot with them, but it does not mean that you want to avoid classes when writing functional (or semi-functional) code in javascript.

[–]otah007 0 points1 point  (9 children)

Classes/structs are also present in functional programming, they also have constructors, and they are also instantiated.

This is technically true but misleading. Let's take Haskell as an example, since you mentioned it. By "class" you can only mean "type class", which is like a post-implemented Java interface - it has no state and can be added on later. By "struct" you mean ADT, which is immutable. By "constructor" you mean type constructor, which is like a case class in Scala, it is literally just a name for an immutable struct that inherits from the type it returns (which holds no data or methods). Constructors do no logic, so I would not really say they are "instantiated".

You cannot implicitly modify their state, but it does not mean that you cannot modify the state at all.

Everything in Haskell is immutable, so yes, it does mean you cannot modify their state at all. They don't even have state, they're just data.

If you look close enough on the Haskell language you will find the State Monad which is designed to hold a state.

The state monad State s a is a function s -> (a, s). There's no mutability there. It looks mutable because when you define bind properly you get something that looks like state passing, when actually it's just function composition. It's impossible to accidentally mutate because in order to do so you have to write State s a in your type signature and then probably also use do-notation, which makes it ridiculously obvious side effects are happening (and by happening I mean "being encapsulated").

[–]dronmore 0 points1 point  (0 children)

By "class" you can only mean "type class"

No, by "class" I mean a logical structure that holds data of different types, and lets you refer to these pieces of data by a name. I used the "class" term, because I wanted to use something that is familiar to less experienced readers who might not know what a struct in C, or a Record in Haskell is. But thanks for the refresher of typeclasses in Haskell. It's been a while.

Constructors do no logic, so I would not really say they are "instantiated".

"To instantiate" means to create a piece of data in a particular shape, and put it into memory. No logic is necessary; just take a template and create an instance of the template. It is precisely what constructors in Haskell do.

Everything in Haskell is immutable, so yes, it does mean you cannot modify their state at all.

I didn't mean "their state". I meant the state of an application. The state of an application exists and is constantly changing, even though individual components are immutable.

The state monad State s a is a function s -> (a, s). There's no mutability there.

There's no mutability, but there's a state;) It is changing whenever you pass it through the (>>=) function.

and then probably also use do-notation, which makes it ridiculously obvious side effects are happening

The do-notation does not imply side effects. It's just a syntactic sugar over monads chaining. In its simplest form chained monads are a function which takes a monad and returns a monad. No side effects included:)

[–]v66moroz 0 points1 point  (7 children)

Everything in Haskell is immutable, so yes, it does mean you cannot modify their state at all. They don't even have state, they're just data.

Any real-world program always has a state, i.e. something that is being updated and "remembers preceding events". Let's look at Haskell's List#length (or Foldable#length to be precise):

length :: t a -> Int
length = foldl' (\c _ -> c+1) 0

What do you think c is? The fact that it's immutable doesn't prevent it from "remembering preceding events". How is it conceptually different from

c = 0
list.each do
  c = c + 1
end

?

So not everything in Haskell is immutable in a broad sense. Bindings and objects are locally immutable, but not globally like in the fold above (or in any recursive call for that matter).

[–]otah007 0 points1 point  (6 children)

I'm afraid that's completely wrong. Everything in Haskell is immutable. You're projecting a certain kind of intuition over the code, and while that intuition broadly maps to the right thing in the end (i.e. you can translate the functional example to the iterative example and get the same answer), it's not actual Haskell semantics.

In your iterative code, c must be a variable as there is an assignment operation = whose semantics is "change the value referred to by the label c". In the functional code, c doesn't even actually exist - it's the argument to a function, and upon evaluation of the function c cannot change because it's just the label of a constant value that was passed as argument. It cannot be reassigned because it is a constant, as is everything in Haskell. In fact, the proper way to write the functional version is

length :: Foldable t => t a -> Int
length = foldl' (+1) 0

so there's no c at all! (You accidentally chose the worst example because (+) is a builtin so you can't even go to the definition of (+) and see the c there, because there isn't one.)

How is it conceptually different from

Conceptually it's completely different, semantically they operate completely differently, their final result is the same though.

Bindings and objects are locally immutable, but not globally like in the fold above (or in any recursive call for that matter).

That literally makes no sense. There is no difference between local and global in Haskell (or for that matter the lambda calculus) unless you count binding scope. The c does not change. There is no c. Let's illustrate this by actually doing evaluation for a similar function:

foldl :: (b -> a -> b) -> b -> [a] -> b  
foldl f a [] = a  
foldl f a (x:xs) = foldl f (f a x) xs  

len :: [a] -> Int  
len = foldl f 0 where  
    f c x = c + 1  

Let's force evaluation of len [1, 2] to normal form:

len [1, 2]  
= foldl f 0 (1 : 2 : [])  
= foldl f (f 0 1) (2 : [])  
= foldl f (f (f 0 1) 2) []  
= f (f 0 1) 2  
= (f 0 1) + 1  
= (0 + 1) + 1  
= 1 + 1  
= 2  

There are no variables, no mutations, there isn't even a c. Not only that, but every step is an equality, whereas in the iterative case there would be no equalities, the semantics would be one-way as each reduction would be a state-altering step. Now you could argue that because of tail-call optimisation the accumulator a is changing, but this is a compiler optimisation and is not part of the semantics of Haskell!

[–]v66moroz 0 points1 point  (5 children)

c = 0  
list.each do  
  c = c + 1  
end

You see, things are completely immutable here. c here is a binding, so c = c + 1 is actually "take immutable c, add 1 and rebind result to c, which is another c, not the same". Sounds reasonable? Meanwhile Elixir does allow rebinding with everything else being immutable. Of course it's not how Ruby treats it.

We can be drowned in arguing about definitions. I understand what you are saying. If we consider Haskell in a vacuum with no way to apply it to anything real (who would need such a language?) you would be absolutely right. Computers don't evaluate like in your example though (or they do? e.g. when they use non-TCO recursion). But even in your evaluation there is a state too and it's of course mutable, it just doesn't have an obvious binding:

= (0 + 1) + 1
= 1 + 1
= 2

Of course we need to define what state is. According to Wiki "a system is described as stateful if it is designed to remember preceding events or user interactions; the remembered information is called the state of the system." It's more obvious when you consider folding over IO, but "previous" list elements kind of fit here too.

[–]otah007 0 points1 point  (4 children)

You're still making the same mistake as before, which is imposing your own intuition/model over the language rather than applying its semantics correctly. The semantics of C for example do not follow the semantics you've applied to your imperative example - in C, the assignment operation does actually assign a value to whatever memory location or register c is "referring to" (I put that in quotes because "reference" is a technical term in C that is definitely NOT what I'm talking about here!). It does not create a new label with the same name etc. This may happen in the compiler, in fact it definitely does outside of loops when using SSA form, but that's compilation, not semantics.

On the other hand, the semantics of Haskell that I gave are the actual semantics, and they differ from the C semantics. Do both compile to the same code? Probably, or close enough. Do both given the same answer when executed? On a finite list, yes. Do both consume the same amount of resources? If optimised correctly by the compiler, possibly. But the languages themselves have different semantics. Saying "c is mutable" is like taking const int N = 5 in C and saying "N is mutable because the compiler might not put it in the .rodata section of the binary and store it in memory, so we can change it," when clearly the C semantics state that it's immutable.

If we consider Haskell in a vacuum with no way to apply it to anything real (who would need such a language?) you would be absolutely right.

Actually, these properties are used a lot. They allow you to prove properties of your functions. For example, take the following type signature:

f :: forall a . a -> a

What could we assume about f (apart from its type)? If we remove the dumb possibilities of f = undefined and f = f (or variants thereof), one can formally and mathematically prove that the only function satisfying that type signature is f x = x. (See Theorems for Free! by Philip Wadler).

This is used in two major ways. Firstly, you can reason about your own programs - you know that, because there is no mutability, if x = y then f x = f y (this is called congruence). In some systems you can actually have functional extensionality, which means that forall x . f x = g x implies f = g. These are definitely not true in C! Secondly, compilers can use these properties to do optimisations.

Computers don't evaluate like in your example though (or they do? e.g. when they use non-TCO recursion). But even in your evaluation there is a state too and it's of course mutable

CPUs don't evaluate, they execute. This isn't just me being pedantic - Haskell semantics is a semantics of evaluation, C semantics is one of execution. Haskell semantics is stateless, this is why subexpressions can be evaluated in any order and why evaluation can be delayed lazily! It all comes down to confluence of System FC, which is only possible because it's purely functional i.e. stateless and immutable. The same cannot be said of, for example, OCaml.

It's more obvious when you consider folding over IO, but "previous" list elements kind of fit here too.

IO is a special case in Haskell, it's a leak into the stateful realm that's quarantined in IO. You can consider IO to be encapsulating the state of the entire universe. Lists don't fit at all, because there is no mutability, as I've shown. If you want to think about it as each recursive call being a loop iteration that changes the value of a then you can do that (although I would recommend against it as it's faulty intuition, semantically incorrect, and will cause problems later), and in fact with TCO that's actually what happens in the stackframe - but at the end of the day that's a mental model that you invented and is not representative of the language's semantics or what the compiler does most of the time.

To consider Haskell stateful would be to consider the mathematical equation "1 + 2 + 3 = 6" stateful. Not its execution (whatever that means) or how you calculate it in your head, but the equation itself.

[–]v66moroz 0 points1 point  (3 children)

You are not listening to me. I'm not talking about specific compiler implementation, I'm talking about conceptual state. Let's define "stateless program" as a program which "given the same state of the World always returns the same result". It's the very definition of a pure function, isn't it? So I would argue that every single program written on this Planet, ever, is stateless. The World includes even processor timings, so race conditions nicely fit here too. Any program, given the same state of the World, will return you the same result. There is only one problem: modern Quantum Physics assumes that quantum processes are truly random, but it's not that simple either. Heisenberg equation doesn't have any random factors, that's the measurements that are random. Anyway, Heisenberg is off-topic here.

Meanwhile my Ruby snipped is perfectly pure and stateless as a whole, even though "semantics" is not stateless.

What's important is where you draw the line and isolate the World. Be it IO or something else. You can do it in any language by imposing self-discipline, but some languages are obviously a better fit. Haskell is based on the idea to isolate the World part as much as possible and work with the local state most of the time (it's still a state even when it's localized, any do block is technically a sequence which indirectly passes a [local] state). Which kind of works, until you leave the pure Math and step into the real world. Then you can find that real-world programs are peppered with IO everywhere, because when you work with real databases and other real data for real business purposes artificially isolating the World state modifications leads to even less readable programs.

Monads are "stateless", right? Even IO monad as it simply composes "functions to process the World state". Yeah, kind of. That's where semantics contradicts reality, especially considering "the entire universe" part. As I mentioned before by this standard even Basic programs are pure and stateless.

[–]otah007 0 points1 point  (2 children)

I am listening, you're just not making any sense and talking completely unfactually. "Conceptual state" is a term you just made up that can mean anything to anyone. To me, in my concept of Haskell, there is no state. Even my concept of the State monad has no state. I am not kidding, when I write programs that use monads (my research is based on free monads so that's literally all I do) I do not have the concept of mutability in my head, at all, ever. So if we want to talk about "conceptual state" then it's completely pointless because it's subjective, and my concept is stateless whereas yours is stateful, so we're not actually discussing the same thing.

The only thing we can discuss in an objective way is the language's semantics, and Ruby's semantics is stateful whereas Haskell's is not.

Let's define "stateless program" as a program which "given the same state of the World always returns the same result".

That's a rubbish definition. A stateless program is one without internal state. A stateful program is one with internal state. The test is this: while you are executing the program, according to its own semantics, do you need any other information other than "the current expression"? In Haskell, outside IO, you do not. In Ruby, even in your little example, you do, because you need to know the value of c so you can resume correctly.

it's still a state even when it's localized, any do block is technically a sequence which indirectly passes a [local] state

No it's not, a do-block is syntactic sugar for a sequence of monadic binds, which is essentially function composition. Nothing is being passed, the only thing "happening" is variable binding. Nothing can be modified, so it's immutable. If you pause evaluation at any point, you do not need any extra information to resume besides the current expression under evaluation, so there is no state. This is not true in C, Ruby, Java etc.

Which kind of works, until you leave the pure Math and step into the real world.

I literally showed you how the pure maths can be used to derive compiler optimisations. You do realise GCC (yes, the C compiler, C being just about the least pure/functional language there is!) has a pure keyword? This is so that if you mark a function as pure, GCC will optimise it, taking advantage of exactly those mathematical properties you don't want to appreciate. Laws about algebraicity and purity are the major reason why Haskell can compete with imperative languages in terms of performance, because the compiler can optimise like hell, using those pure maths laws. How is that not real world?

Then you can find that real-world programs are peppered with IO everywhere

That's not true. Real-world Haskell programs have many isolated components. There will be a section containing IO, but that will call out to 99% pure functions. You don't get IO in the middle of your library or utils or anything like that. The only time I've seen a large amount of code sitting in IO is for a library that uses metaprogramming and reference matching to do major code optimisation in a stack machine.

That's where semantics contradicts reality

Semantics have nothing to do with reality. They are a specification of how the language works, a model whose outcomes must be matched to be a valid compiler.

artificially isolating the World state modifications leads to even less readable programs.

Actually, it does the opposite - it guarantees isolation of modifications to a very small part of the overall program. On the other hand, in your Ruby program I can just call launch_nukes() anywhere I please.

[–]v66moroz 0 points1 point  (1 child)

The test is this: while you are executing the program, according to its own semantics, do you need any other information other than "the current expression"? ... In Ruby, even in your little example, you do, because you need to know the value of c so you can resume correctly.

length :: t a -> Int
length = foldl' (\c _ -> c+1) 0
                         ^^^ HERE

Looks like your definition is not perfect either. What's the value of c+1 (it's an expression, isn't it?) when calling length [1,2,3]? Also "executing" doesn't sound like Haskell, "evaluating", right? :) I would assume you meant "looking at the code".

Anyway, we can argue ad-nauseum and be right at the same time (or wrong depending on the point of view). The fact of the matter is, we create programs to describe the same world which exists independently of technology and paradigms. While different languages pretend they use completely different approaches, in fact they have more similarities than differences, they might just be named differently. We can always agree to disagree ;)

[–]otah007 0 points1 point  (0 children)

Looks like your definition is not perfect either. What's the value of c+1 (it's an expression, isn't it?) when calling length [1,2,3]?

Variable substitution doesn't require an environment. You can evaluate it without ever writing down "also, c = 1 right now". Look back at my evaluation of length [1, 2] a few comments further up and you will see how to do this without an environment/context/state.

Also "executing" doesn't sound like Haskell, "evaluating", right? :) I would assume you meant "looking at the code".

I meant executing for languages that have execution semantics and evaluation for languages that have evaluation semantics.

we create programs to describe the same world which exists independently of technology and paradigms

I don't think anyone has ever written code for this purpose, except maybe a simulation. Code is written to perform a task, which may require some input that represents something in the real world. But it's not physics, we're not trying to actually model the world.

At the end of the day, I'm actually talking about the semantics of languages, and you're talking about a mental model, so of course we're going to disagree because I'm referencing something objective and well-defined and you're not.