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

Effekt by example #536

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
Draft

Effekt by example #536

wants to merge 19 commits into from

Conversation

dvdvgt
Copy link
Collaborator

@dvdvgt dvdvgt commented Aug 5, 2024

This PR aims to add literate Effekt files showcasing the various interesting aspects of Effekt. The main target audience are researchers (including, most notably, compiler engineers). I mostly tried to limit explanations to purely syntactical aspects. Considering our target audience, the semantics should mostly be straightforward to understand.

Table of contents

Here are the planned and partially already drafted topics:

  • Hello World (println, inspect)
  • Primitive Values
  • Strings and characters
  • Records
  • ADTs & pattern matching
  • Mutable variables
  • Loops
  • Functions
    • local function definitions
    • known jumps
  • Effects & handlers(*)
  • Effect collection?
    • Non-deterministic
    • ...
  • Objects
  • Blocks & Computations(*)
  • Boxing(*)
  • Namespaces
  • Modules
  • File! IO
    • show word count example, load and read file
  • Regions(*)
  • FFI

Topics marked with a * are considered difficult and yet the most crucial topics of this collection.

TODO

  • Perhaps include more citations

Changelog

  • Added foldLeft and foldRight function in the standard library for lists
  • Fixed a bug causing the lexer to crash when encountering unmatched curly braces

Request for feedback

Please do leave feedback. It is surprisingly difficult to gauge what to explain and mention and what to leave out.

While writing these examples, I may extend the standard library with new functions. It would be nice to keep an eye on those and discuss their inclusion.

Disclaimer: I am aware that there may be grammatically errors and typos. The formatting (e.g. limiting line width to ~120 characters) partly is subpar as well. Since much is still in flux, I have not bothered yet to go through the prose in more detail. When the rest is finalised, I will take care of these issues.

Future Work

Upon closer inspection, you may notice that the fenced code blocks not only have the types like effekt:repl but also

  • effekt:hide: This content is not visible on the website
  • effekt:clear: The following content is not compiled together with previous.
  • effekt:def: This is the default when there is no type after three ``` and contains definitions.

These are preprocessing directives mainly targeting the website and still need to be implemented.

@dvdvgt dvdvgt added documentation Improvements or additions to documentation draft Draft PR, do not merge! labels Aug 5, 2024
Copy link
Contributor

@jiribenes jiribenes left a comment

Choose a reason for hiding this comment

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

This looks like a great start! I read through some of these and wrote down a few notes in the review (whilst trying to keep your disclaimer in mind :))

Comment on lines +210 to +217
// Fold a list using `f`, starting from the left given a starting value.
//
// O(N)
def foldLeft[A, B](l: List[A], init: B) { f: (B, A) => B }: B = {
var acc = init;
l.foreach { x => acc = f(acc, x) };
acc
}
Copy link
Contributor

@jiribenes jiribenes Aug 5, 2024

Choose a reason for hiding this comment

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

Does this work on the MLton backend (mutable variable used in a foreach)?
Anyways — it would be nice to put a few tests for these new functions into the stdlib tests :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This how other functions like sum are also defined, so I just assumed this works

examples/features/values.effekt.md Outdated Show resolved Hide resolved
examples/features/values.effekt.md Outdated Show resolved Hide resolved
examples/features/records.effekt.md Outdated Show resolved Hide resolved
examples/features/records.effekt.md Outdated Show resolved Hide resolved
examples/features/adts.effekt.md Outdated Show resolved Hide resolved
val packet = do receive[Job]()
packet match {
case Some(p) => println("processing request")
case None => do break()
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that from #528 onward, you'll need to bind the label:

each(0, 100) { (i) {label} =>
     total = total + i;
     if (total > 15) label.break()
     println(i)
}

examples/features/loops.effekt.md Show resolved Hide resolved
examples/features/loops.effekt.md Outdated Show resolved Hide resolved

```effekt:repl
var x: Int
```
Copy link
Contributor

@jiribenes jiribenes Aug 5, 2024

Choose a reason for hiding this comment

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

In the future, it might be nice to have a forward reference to regions right here.

@dvdvgt
Copy link
Collaborator Author

dvdvgt commented Aug 6, 2024

I just noticed that all tests in examples/stdlib either have a module path that is not consistent with the folder structure or none at all. Does this matter?


```
interface Exception {
def throw(msg: String): Nothing
Copy link

Choose a reason for hiding this comment

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

What is this Nothing type?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We have a very flat subtyping lattice in Effekt. Nothing is bottom in this lattice and thus is a subtype of everything. However, there is no inhabitant of this type. Yet, it is still useful. It is commonly used as the return type for functions that do not return. This is exactly what happens in other languages with exceptions. throw will never return anything.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the question! We could also explain that somewhere ("it's the type for unreachable things") -- it might even be worthy to say that it's Rust's & TypeScript's never, C++'s noreturn.

def containsFile(fs: FileSystem, name: String): Bool =
fs match {
case File(name1, _) and name1 == name => true
case Directory(_, children) =>
Copy link

Choose a reason for hiding this comment

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

It would be helpful to note that brackets {} do not currently work in case expressions, unless that is going to change soon...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I am not exactly sure what you mean. The body of each arm may be a single expression or a statement delimited by curly braces. So what you are describing should work unless I misunderstood your remark. Feel free to elaborate in this case

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That is, you also could have written { true } as the first match arm's body.

Copy link
Contributor

Choose a reason for hiding this comment

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

see #422, I don't think the new parser resolved this

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ha, thanks. I would have expected this to work. This should be easy to support if we want to.

examples/features/blocks.effekt.md Outdated Show resolved Hide resolved
try {
div(a, b)
} with Exception {
def throw(msg) = {
Copy link

Choose a reason for hiding this comment

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

Question: there is a fair amount of code out there with handlers as anonymous functions, i.e.

try div(a, b)
with Exception {
  (msg) => panic(msg)
}

Is this deprecated, or is it limited syntax sugar? It is decently widespread enough in other documentation that it may be nice to mention.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good question!

If we had defined the Exception effect as follows

effect Exception(msg: String): Nothing

we could have used the syntax you described. However, this is IMO in a bit of a weird place right now because of the different keyword (effect vs interface). I am therefore reluctant to mention it. I'll give it some thought.

Copy link

Choose a reason for hiding this comment

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

Oh interesting, okay! That clears up a lot of confusion I had. I thought effect Exception(msg: String): Nothing was a direct alias to interface Exception { def Exception(msg: String): Nothing } and couldn't figure out why it wasn't working...

}
```

The operations `break` and `continue` have their usual semantics, except that they are handled by `loop` as operations of the algebraic effect `Control`.
Copy link

Choose a reason for hiding this comment

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

As a newcomer to effects, the semantics of break/continue don't actually line up very well with my mental model of how effects systems work...

  • Do while and loop both use the same Control effect, or do break and continue only function within loop?
  • Does the while construct desugar into some sort of handler, and if so, is it installed at the top level or wrapped around the while computation?
  • Can you call continue within a function called within a loop and have it continue the external loop?

Copy link
Collaborator Author

@dvdvgt dvdvgt Aug 10, 2024

Choose a reason for hiding this comment

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

Thanks for taking your time to ask these insightful questions! It is very valuable for us to get feedback for newcomers to algebraic effects.

  • while is a natively built-in constructor and does not use the Control effect by default. However, no one is stopping you from wrapping a while loop using this effect in a handler and defining your own behaviour :)
  • See previous answer. Interestingly, loop is a handler. I invite you to look at the (relatively concise) implementation to perhaps reconcile it with your mental model:
    https://github.com/effekt-lang/effekt/blob/master/libraries%2Fcommon%2Feffekt.effekt#L651-L677
  • Yes, indeed. You can do that. Note that the semantics of Control depend on the lexically nearest handler. If there is one "deeper nested" than loop, that one will handle Control instead. This is true for all effects, of course.

Feel free to ask further questions, should anything be unclear to you.

Copy link

Choose a reason for hiding this comment

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

Cool! Is there any reason you all decided to separate out while into a built-in primitive in the first place?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I am not aware of any particular reason. I would guess that while loops map nicely to some of the backends and can leverage the respective backend's optimisations.

Comment on lines 20 to 25
Perhaps unusual, you can also call `fib` using Effekt's implementation of [Uniform Function Call Syntax
](https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax#cite_note-4):

```effekt:repl
5.fib
```
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like this is subtly combining two different things:

  1. UFCS
  2. the ability to omit parens for functions without parameters

Could we split it into two? Show 5.fib() first and then show the parenthesis-omitting part some time later?

}
```

As we have established in a previous section, functions in Effekt are second-class. While you cannot pass them as first-class values to other functions as argument, you can pass them as second-class blocks.
Copy link

Choose a reason for hiding this comment

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

I might suggest moving this whole section into the functions.effekt.md section... it's not entirely clear at the moment how UFCS and blocks interact (i.e. I was surprised that you can't do { 5.div(0) }.default(0))

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point, I will add some explanation for blocks and UFCS

@dvdvgt
Copy link
Collaborator Author

dvdvgt commented Sep 8, 2024

Thanks to @jiribenes for workshopping the captures and regions sections with me!

Copy link
Collaborator

@b-studios b-studios left a comment

Choose a reason for hiding this comment

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

I just looked into the FFI explanation and already like it a lot. I just left a few comments.

@@ -0,0 +1,119 @@
# FFI
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
# FFI
# FFI (Foreign Function Interface)

Comment on lines +5 to +6
Effekt allows defining extern functions via FFI.
Let's start with the JavaScript backend by defining a function that adds two numbers:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Effekt allows defining extern functions via FFI.
Let's start with the JavaScript backend by defining a function that adds two numbers:
Effekt allows communicating with the host platform (for instance JavaScript) by defining extern functions via FFI.
Let's start with the JavaScript backend by defining a function that adds two numbers:


## `extern include`

Sometimes, you want to include a whole JavaScript files containing your favourite JavaScript library
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Sometimes, you want to include a whole JavaScript files containing your favourite JavaScript library
Sometimes, you want to include a whole JavaScript file containing your favourite JavaScript library

extern include js "./mycoollibrary.js"
```

> NOTE: Do not run this code on the website ^
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you explain why? Something like: "since these paths are relative, includes can only be used in local projects, but not on the Effekt website."

Comment on lines +44 to +48
extern js """
function set$impl(ref, value) {
ref.value = value;
return $effekt.unit;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
extern js """
function set$impl(ref, value) {
ref.value = value;
return $effekt.unit;
}
extern js """
function set$impl(ref, value) {
ref.value = value;
return $effekt.unit;
}

Please do not use tabs.

You can and should annotate the capture of your extern function as `extern <capture> def ...`

- no capture, usually denoted as `pure`, also writable as `{}`
- if you want to allocate into a global scope, use `global`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
- if you want to allocate into a global scope, use `global`
- if you want to allocate into a global scope (e.g. the JavaScript heap), use `global`

Comment on lines +65 to +66
- if your operation does I/O (like for example `println`), use `io` (this is the default capture)
- if your operation does other control effects, use `control`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
- if your operation does I/O (like for example `println`), use `io` (this is the default capture)
- if your operation does other control effects, use `control`
- if your operation performs I/O (like for example `println`), use `io` (this is the default capture)
- if your operation captures the continuation use `control` (in the JS backend, control definitions should return a monadic value of type `Control`)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation draft Draft PR, do not merge!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants