-
-
Notifications
You must be signed in to change notification settings - Fork 77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Post: Functional Crystal #373
base: master
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for crystal-website ready!
To edit notification comments on pull requests, go to your Netlify site settings. |
|
||
### Values and references | ||
|
||
Compilers achieve immutability passing elements _by value_ instead of _by reference_. Consider the following code: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe mention that by value means that when passing a value it's duplicated and independent of the original?
arr[0] += 1 | ||
arr = [2] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest to maybe leave out the thing about the return value. The value of g(z)
is never used, so that arr = [2]
doesn't matter.
Also, instead of arr[0] += 1
I'd suggest to go with arr += [1]
for less indirection and closer resemblance of the as value case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest to maybe leave out the thing about the return value.
Sorry, I don't follow. There's no discussion about the returned value.
def translate_x(point : Point, x : Int32) | ||
point.x += x | ||
point | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be an option to move this method to an instance method Point#translate
?
I know this may not be purely functional, but it's technically still a better way to organize code IMO.
This might interfere with the following thoughts, so it's might be better as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative could also be the record
macro and its #copy_with
method. Would be more concise and highlight that Crystal has tools for this included.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm... thinking a bit more about this: the problem is that copy_with
is not showing where the copy is done. Here, the fact that we're passing the point as argument of translate_x
is what's producing the copy transparently. I'll leave it as is, and add a comment.
|
||
Note how this method is more concise than the OOP one (there's a little white lie, see the coming section). Also importantly, note how we're casing on the type of `self`. | ||
|
||
## The missing bits |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for this section it would be important not to state the status quo, but also express at least some ideas for future developments. Even stating that some aspect may be unlikely to change would at least give an expectation.
This can definitely be strongly opinionated! This might put you in a bit of a tough spot because as team lead you might be inclined to make realistic predictions😅 . But it is important for the article to give some outlook on these matters.
With the `case` statement we can directly code the printing in the `inspect` method of `Ast`. This is a more general example than what we saw above with `ParseResult`, this time traversing the recursive structure of an `Ast`. | ||
|
||
```cr | ||
abstract class Ast | ||
def inspect(io) | ||
case self | ||
in BinOp | ||
self.left.inspect io | ||
self.operator.to_s io | ||
self.right.inspect io | ||
in IntLiteral | ||
self.number.to_s io | ||
end | ||
end | ||
end | ||
``` | ||
|
||
Note how this method is more concise than the OOP one (there's a little white lie, see the coming section). Also importantly, note how we're casing on the type of `self`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, this part makes me feel a bit uneasy. Sure internalizing the reference to io
removes side effects that the visitor pattern uses. But then you're just trading multi dispatch for exhaustive case, which might be fine for this simplified use case, but when scaling that up it will lead to complex code in a huge case
expression.
I don't have a better example at hand, but maybe we can find something more suitable to present pattern matching? (maybe also consider case
with a tuple for that?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking this exact same thing while I was preparing mate this morning. In particular, I don't want to come across as if the compiler should adopt this instead, precisely for what you mean.
|
||
A missing piece is that structs are placed in the stack, unlike classes, and that is why the compiler needs to know ahead of time the exact size of it. | ||
|
||
If we want to have immutable classes, that are placed in the heap instead of in the stack (and therefore, can have recursive instances), the compiler should add support for it. One way to achieve that is to have the compiler call a method when passing `Reference` objects around, and have `Reference` return `self` and a new `ImmutableReference` type of object return a copy. If it sounds costly, fear not as the LLVM backend should optimize it in `--release` mode. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to have immutable classes, that are placed in the heap instead of in the stack (and therefore, can have recursive instances), the compiler should add support for it. One way to achieve that is to have the compiler call a method when passing `Reference` objects around, and have `Reference` return `self` and a new `ImmutableReference` type of object return a copy. If it sounds costly, fear not as the LLVM backend should optimize it in `--release` mode. | |
If we want to have immutable classes, that are placed in the heap instead of in the stack (and therefore, can represent recursive structures), the compiler should add support for it. One way to achieve that is to have the compiler call a method when passing `Reference` objects around, and have `Reference` return `self` and a new `ImmutableReference` type of object return a copy. If it sounds costly, fear not as the LLVM backend should optimize it in `--release` mode. |
I don't get this idea about a compiler feature. If you want an immutable class type, you can get that by just not defining any setter or mutating its instance variables in any other way.
This requires discipline when defining types, which might be sub-par. But it's definitely possible to get immutable types even without any additional compiler features.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I follow. Of course you can build stuff for each particular case, but the point of a compiler should be to make it easier for you to code certain patterns. This said, I now disagree with my proposed solution and more so to the fact that I even added a solution. That's not the point of this article 🤦
mpettinati left a comment: should be: Browser metadata
|
mpettinati left a comment: change <See how important it is immutability: we know that if we do not change locally zero, it will always have the value (0, 0). Exercise: change struct with class and see what happens.> to See how important immutability is: we know that if we do not change zero locally, it will always have the value (0, 0). Exercise: replace struct with class and see what happens. Browser metadata
|
mpettinati left a comment: <The OOP approach: The Visitor Pattern Browser metadata
|
…Ast to Food, splitted and moved 'the missing bits' section into each relevant part, and clearing up that this is not necessarily an agenda
author: beta-ziliani | ||
--- | ||
|
||
If someone posts a claim "_Crystal is a functional language_", they would be given a grim look. I mean, classes and inheritance are all over the place in the stdlib. Surely it's an object oriented language, right? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be shortened to: "if someone claims ..."
|
||
If someone posts a claim "_Crystal is a functional language_", they would be given a grim look. I mean, classes and inheritance are all over the place in the stdlib. Surely it's an object oriented language, right? | ||
|
||
If we observe modern programming languages, we'll find that neither functional languages are strictly functional, nor object oriented ones are strictly object oriented. Or, to put it differently, these two concepts are not in contradiction; they can both be part of the tools provided by languages to abstract code. And Crystal is not an exception! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"observe" can be simplified to "look at".
"Or, to put it differently" is an unneded sentence here.
|
||
If we observe modern programming languages, we'll find that neither functional languages are strictly functional, nor object oriented ones are strictly object oriented. Or, to put it differently, these two concepts are not in contradiction; they can both be part of the tools provided by languages to abstract code. And Crystal is not an exception! | ||
|
||
But why should you care? As it turns out, functional programming got fancy these years for several reasons, which can be simplified as: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Which can be simplified as" should be removed.
2. Algebraic data types. | ||
3. Closures. | ||
|
||
While discussing each of these, we will point out at some improvements that could be made to the compiler and stdlib to improve its support. The intention is not to fix an agenda, but rather to open the discussion about wether such improvements are relevant to the community. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"we will point out at some"
"at" is a typo here.
|
||
## Mutability considered harmful | ||
|
||
A key aspects of functional programming is related to the idea that any change in the state of the program should be _local_. A way to describe this is with the following comparison, where `f` is any function or method, and `x` any value: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"aspects" shold be single.
|
||
Changing (_mutating_) the count at each call of `f` is a _non-local effect_, as it doesn't depend on exclusively on `f`'s arguments. You could say that the example above is tricky, but I'm certain that you've experience before the surprised of calling a method and getting an unexpected result. | ||
|
||
Several languages that consider themselves functional allow such _non-local effects_. These languages are sometimes mentioned as _multi-paradigm_ or _hybrid_, and an important part of this post is to show that Crystal is also such an hybrid language. It definitively favors OOP, but it also allows important functional patterns. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Several can be removed. Start with Languages
I really like how this post is coming together! I suggested a few changes just now and will come back and add more in a few hours |
<br/> | ||
<br/> | ||
|
||
The answer is in the `Array`'s type: it's a `class`, so it's passed _by reference_. That is, changes performed in the array within the function affects `z`. Now, _the reference_ of the array itself (where `z` is located in the memory) is passed by value. Essentially, this means that the array itself is **not** replaced when assigning the empty array to `arr`. Therefore, the answer is `[0, 1]`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is false; as arr += [1]
expands to arr = arr + [1]
, both lines of g
's body merely assign to the arr
parameter, and the answer is [0]
. Probably the snippet meant to use #concat
instead.
If the audience of this post includes general non-Crystal programmers, it might be helpful to note somewhere that (extended) assignment operators do not exist in Crystal like they do in C++ (operator+=
) or Rust (std::ops::AddAssign
).
|
||
```cr | ||
struct Point | ||
getter x : Int32, y : Int32 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getter x : Int32, y : Int32 | |
property x : Int32, y : Int32 |
end | ||
``` | ||
|
||
Also: the exhaustive checker of `case` in the example [requires a case for the abstract class `Food`](https://github.com/crystal-lang/crystal/issues/12796). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This limitation goes away if Food
becomes a module instead. Might be worth mentioning because we could get rid of inheritance altogether
|
||
1. **A more functional stdlib** | ||
|
||
The stdlib pervasively raises exceptions, and besides nilable types, has little support for exceptions-as-values. This can be done in baby-steps, like adding the `Object#pure!` and `Object#chain` methods discussed in this post. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personal opinion: an additional limitation is that, without disjoint unions, Hash(K, V?)#[]?
cannot distinguish between an existing nil value and a missing value, whereas a true Result
type could, so we have to use alternative APIs (e.g. #fetch
) if we consider our union to be the equivalent of Result
record Point, x : Int32 = 0, y : Int32 = 0 | ||
``` | ||
|
||
`record` generates an identical struct as above, together with a handy method `#copy_with` that allows to return a copy of the struct with some given instance variables modified. For instance, we can solve Exercise 2 above defining the `translate_x` method directly when defining `Point`: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the above example uses property
instead of getter
then theseare not identical structs, as only the one above is mutable
No description provided.