Skip to content
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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

beta-ziliani
Copy link
Member

No description provided.

@netlify
Copy link

netlify bot commented Dec 2, 2022

Deploy Preview for crystal-website ready!

Name Link
🔨 Latest commit 83de9d0
🔍 Latest deploy log https://app.netlify.com/sites/crystal-website/deploys/639cba92eeddbc000970da7b
😎 Deploy Preview https://deploy-preview-373--crystal-website.netlify.app/2022/11/11/functional-crystal
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

@beta-ziliani beta-ziliani marked this pull request as ready for review December 2, 2022 21:30

### Values and references

Compilers achieve immutability passing elements _by value_ instead of _by reference_. Consider the following code:
Copy link
Member

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?

Comment on lines 79 to 80
arr[0] += 1
arr = [2]
Copy link
Member

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.

Copy link
Member Author

@beta-ziliani beta-ziliani Dec 6, 2022

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.

Comment on lines 115 to 118
def translate_x(point : Point, x : Int32)
point.x += x
point
end
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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
Copy link
Member

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.

Comment on lines 328 to 345
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`.
Copy link
Member

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?)

Copy link
Member Author

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Copy link
Member Author

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 🤦

@netlify
Copy link

netlify bot commented Dec 5, 2022

mpettinati left a comment:

should be:
Space purposefully left blank to let you think.

Browser metadata
Path:      /2022/11/11/functional-crystal.html
Browser:   Chrome 107.0.0.0 on Mac OS 10.15.7
Viewport:  1680 x 888 @2x
Language:  en-US
Cookies:   Enabled

Open in BrowserStack

Open Deploy Preview · Mark as Resolved

@netlify
Copy link

netlify bot commented Dec 5, 2022

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
Path:      /2022/11/11/functional-crystal.html
Browser:   Chrome 107.0.0.0 on Mac OS 10.15.7
Viewport:  1680 x 888 @2x
Language:  en-US
Cookies:   Enabled

Open in BrowserStack

Open Deploy Preview · Mark as Resolved

@netlify
Copy link

netlify bot commented Dec 5, 2022

mpettinati left a comment:

<The OOP approach: The Visitor Pattern
The compiler of Crystal uses> should be

Browser metadata
Path:      /2022/11/11/functional-crystal.html
Browser:   Chrome 107.0.0.0 on Mac OS 10.15.7
Viewport:  1680 x 888 @2x
Language:  en-US
Cookies:   Enabled

Open in BrowserStack

Open Deploy Preview · Mark as Resolved

…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?

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!

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:

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.

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:

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.

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

@a-alhusaini
Copy link

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]`.
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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).
Copy link
Contributor

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.
Copy link
Contributor

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`:
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants