#Curious: Why can’t `use` be last, while `let` can?

1 messages · Page 1 of 1 (latest)

ember rain
#

I’m learning Gleam, and I’m messing around with the syntax to get more familiar with it.

Today I learned that the last statement of a function or block can be a let. (Coming from F#, I did not expect that.)

Out of curiosity, I tried having a use as the last statement. That’s not ok, it emits a warning and crashes at runtime.

Is there a reason for this? Not that it matters, I’m just curious 😄 For some reason, edge cases is a fun way of learning for me.

import gleam/io

fn f1() {
  // Ok, returns "joe"
  let _ = "joe"
}

fn f2() {
  // Not ok, emits warning and crashes at runtime
  use _ <- g
}

fn g(cb) {
  cb("joe")
}

pub fn main() {
  io.println("Hello, " <> f2())
}

Btw, the warning is “A use expression must always be followed by at least one more
expression.” But use is not an expression, it’s a statement, isn’t it? Because it is not allowed where any expression is, it’s only allowed in functions and blocks, right? Just like let.

west socket
#

Basically, you need to provide the anonymous function body when using use. In your case, you're telling use to call g with a callback, but you're not actually providing that callback

minor locust
#

everything inside a function is an expression, which includes both let and use. gleam does place some syntactic restrictions on where those two forms can be used but this is for clarity, they are still expressions and they still evaluate

#

eg you can write

wibble({ let x = 10 })
weary moth
ember rain
minor locust
#

yes

west socket
#

Essentially, yes 🙂

minor locust
#

not essentially, exactly!

ember rain
#

But for let it was decided not to do:

fn f1() {
  let _ = "joe"
  // no body here, that’s not allowed
}

but instead:

fn f1() {
  let _ = "joe"
  // we’re gonna return the right side of that assignment here for ya
}

?

minor locust
#

binding is an expression that evaluates to the value being bound, yes

ember rain
#

I’m actually a bit confused about expressions and statements. Looking at the source code of the compiler, it’s actually called statement: https://github.com/gleam-lang/gleam/blob/426e729ab5ae6f59596885c80e0d3a74a5b56938/compiler-core/src/ast.rs#L2174-L2183

And I would argue that let isn’t an expression like any other, because this is not valid:

fn f1() {
  "j" <> let _ = "oe"
}
GitHub

⭐️ A friendly language for building type-safe, scalable systems! - gleam-lang/gleam

minor locust
#

the name of something in the compiler does not indicate its semantics

#

like i said before, both let and use are expressions, with a syntactic restriction that disallows them in some contexts for clarity

west socket
#

This is valid. It's a scope thing

fn f1() {
  "j" <> { let _ = "oe" }
}
minor locust
#
type BlockScopedExpression
  = Use (List Pattern) Expression
  | Let Pattern Expression
  | Expr Expression

type Expression
  = Call Expression (List Expression)
  | Binop Expression Op Expression
  | Block (List BlockScopedExpression)
  | ...

you can imagine the expression language is this

ember rain
#

Yeah, so there’s two types of things then, regardless of what they are called

minor locust
#

if you want to slice it that when its "there are two types of expressions, those that are only valid in blocks and those that are valid anywhere". neither are statements, because nothing inside a function is a statement

#

statements are language constructs that do not produce values, and gleam has none of those

#

its either a syntax error or its an expression

ember rain
#

But use does not produce a value? I would argue let doesn’t either, it was just decided that a return value can be extracted from it if it happens to be last in a function or block. But we’re splitting hairs 😄

modest stream
#

use does produce a value though

minor locust
#

as they say, its splitting hairs

#

(:

modest stream
#

I know but that part isn't really correct 😄

ember rain
#

I think I need to google what an expression is, it’s probably just my understanding that is a bit flawed too. Thanks for all the answers!

minor locust
#

i think probably you are getting tripped up by conflating the syntax rules of the language with the semantics

#

honestly the surprising bit is the formatter not inserting a todo for incomplete use expressions the same way it would for incomplete fns

ember rain
#

Oh, cool, didn’t know it did that for incomplete fns!

#

Btw, that let that isn’t followed by a “regular” expression isn’t a syntax error seems really nice, that can be pretty annoying in F#. There I have to defensively add something after the let while writing the let to make the tooling not be angry

ember rain
#

Ok, I think I finally get use as an expression now.

So, when parsing it’s possible to represent a function body as a list of things, one of which is a use. But … semantically? … a use isn’t just that use AST node, it’s that node plus all of the things that come after it in the function. Which is clear when you desugar it.

fn f() {
    use a <- g
    a + 1
}

The way I though about it before was that use a <- g was one thing, and a + 1 was another, a sibling. (Which seems to be how it’s represented in the AST.) I was thinking that the use only had the effect of binding variables for use in later expressions. But: It also affects the entire return value, since it’s a function call and we actually return the result of that call.

So when thinking about the value of it, I’m thinking more that a + 1 is a nested expression inside the use expression, just like in the desugared form:

fn f() {
    g(fn(a) {
        a + 1
    })
}

That’s why the warning says “Incomplete use expression”.

In an alternate universe, the AST could have reflected that too, I guess (extending Hayleigh’s example, sorry I keep using Elm syntax here…):

-- Represents a function body, `{ stuff }`, and the things following a `use`
type alias Block =
    { expressions : List BlockScopedExpression
    , use : Maybe Use 
    }

type BlockScopedExpression
    = Let Pattern Expression
    | Expr Expression

type alias Use =
    { patterns : List Pattern
    , expression : Expression
    , nested_expressions : Block -- warning if empty, note: no braces here
    }

type Expression
    = Call Expression (List Expression)
    | Binop Expression Op Expression
    | Block Block -- this is the syntax with braces
    | ...
rotund egret
#

Would it be fair to say that the reason you can't use use at the end is because effectively, the line after use statement is the "callback" for the (in OP's example) g function. So Omitting the line is like omitting a parameter. And Gleam does not support optional paramters.

Is this a fair summary?

minor locust
#

you just haven't finished the expression

#

in the same way that 1 + is an incomplete term, or let x = is also incomplete

rotund egret
#

Yeah, but use is just sugar to avoid callback hell, right?

minor locust
#

ommitting the body is not like ommitting a parametr

#

its like writing fn(x) {}

#

which will have the same behaviour, by the way (compile, warn, runtime crash)

rotund egret
#

Got it, thanks @minor locust

minor locust
ember rain
#

🤯
lydell: joins Discord
hayleigh, two seconds later: 👀
lydell: asks silly question
gleam folks, less than 1 day later: updates formatter to clarify

minor locust
#

its nice when things are actively maintained huh

#

we're always on our best behaviour to poach elm people you see