I like the idea overall. Not needing to convert between Result and Task is a pretty nice bonus!
I wonder if it would make more sense to annotate effects using the arrow instead of a bang in the result type, given it's the function itself that's effectful.
So instead of this: {} -> Result! U64 Str
We might have this: {} !> Result U64 Str
(I don't know if !> is the best operator here or something else would be better, you get the idea)
If an additional rule is that in a type signature with multiple !> operators all those operators have the same implicit effect parameter, then there's no need for a separate => operator, so that might reduce the syntax footprint a bit.
Plus, it'd avoid needing to explain why this isn't a valid type signature:
effectfullNumber: U64!
@Jasper Woudenberg I think -!> implies that something is happening during the creation of a return value, but I'm generally on the same page.
yeah I agree that it would be nicer if the arrow itself described the effectfulness (or not), although I'm personally not a fan of !>
I'm also not generally a fan of infix operators that take more than 2 characters
the three that seem reasonable to me are -> and => and ~>
another problem is that there has to be syntax for "named effectfulness variable" (e.g. !fx in the doc) - even if it comes up super rarely in practice, syntax for it has to exist for type inference to work
(otherwise, for example, you could put valid code into the repl which happens to have those relationships and we just couldn't print an inferred type for it)
and I'm not sure how to sneak a variable into the arrow haha
that said, I also separately like the idea of effectfullNumber: U64! being something you can't even write syntactically
Jasper Woudenberg said:
If an additional rule is that in a type signature with multiple
!>operators all those operators have the same implicit effect parameter, then there's no need for a separate=>operator, so that might reduce the syntax footprint a bit.
in that world, we could replace ! with => and then have:
-> means pure function=> means "potentially effectful function, although it could be pure if it's higher-order"Why is => needed? If you have a higher-order function like:
List.map : List a, (a -> b) -> List b
The only way for List.map to return a b is by calling the passed function, so we can infer that it's equally effectful as the passed function.
there's a section in the doc on that
Oh, I didn't see that in the latest version
I agree, even though the doc covers it, I don't think it's that big of a deal that an effectful (a -> b) can be used, it will just propagate up an async section.
But the cost of this is now people have to write functions knowing that they're handling async-able functions
I'm worried it will create an unnecessary split in the ecosystem, too
maybe not so much a split, but it'd be frustrating if you were using a higher-order function from a library and you couldn't use it with effects because the author forgot to use =>
So I'd say unless it's really a big issue that we just function color things all the way down, the cost is what Agus points out, which is that the default way to use functions is now overly constrictive for potentially async usage, and the special way is better but requires an understanding of async coloring
here's the section I had in an earlier draft, regarding that idea:
The proposal here is to make the current
List.maptype signature Just Work (that is, be effect-polymorphic) by adopting the following rule:If a higher-order function type annotation contains exactly one function type, then it is effect-polymorphic.
This rule preserves being able to tell whether a function is pure by looking only at its type:
* if there's a!after its->then the function is definitely effectful
* if there's a!type variable after the->or if the function type has exactly one other function (and that function is an argument, not in the return type) then it's effect-polymorphic and whether or not it performs effects will depend on what type you pass it at the call site.
* otherwise, it's a pure functionImportantly, this rule is about function types that appear visibly in the type annotation. Function types that are behind a type alias or opaque type don't apply to this rule.
This rule lets common higher-order functions have types that look the same as today, while being more useful than today because they're effect-polymorphic.
so the rule in the doc for => definitely feels simpler to explain: "their effectfulnesses are equal"
At that point, would it maybe be better to have -> now act as => does, and -> act as a "pure-only" version?
If possible, the default should be the best option, so the default function signature should be more general if possible
yeah so one potential variation of this "higher-order functions are special" design is to say that "-> always means pure" and "=> sometimes means effectful but not always" (instead of the other way around)
Yeah, if that's possible to implement in a way that doesn't require much thought from the user and that could be the default function type we teach in the tutorial, then I'm in
so there's a broad question of which of these three designs is better:
->, =>, and ! and they all mean different thingsDoes your design handle multiple different colorings? What about:
doubleMap : a, (a => b), (b => c) => c
I think you'd need the explicit type variable in that case
depends on what you want it to do
Yeah, yeah
you wouldn't need the explicit type variable if both functions are being called
I'm trying to come from a perspective of "How do we make things Just Work:tm:"
because they all have the same effectfulness
but what if you pass an effectful one and a non-effectful one?
This puts the pressure in the higher-order function author, but they might not know how it's used
Agus Zubiaga said:
but what if you pass an effectful one and a non-effectful one?
still the same
non-effectful unifies to effectful
ah, nice
I think if you try writing it out with type variables it'll be come apparent :big_smile:
like if both are being called, and they have different variable names, how would you put both variable names in the overall function's return type at the same type?
(you wouldn't, you'd use the same variable name for all of them)
to go back to Jasper's original point about trying to remove ! in favor of changing the arrow, here is one option that takes the same semantics as the current proposal but with different syntax:
List.first : List elem -> Result elem [ListWasEmpty]
List.map : List a, (a ~> b) ~> List b
File.readUtf8 : Str => Result Str [FileReadErr File.ReadErr]
I don't love how ~> looks though haha
So -> means pure, ~> means equally efffectful, and => means effectful?
My worry with -> vs => is that a newcomer won't really get that there's much of a difference. I feel like ~> or something like it implies something special happening, which I'd say effects are
I guess I like three arrow operators over two arrows and a bang
I feel like that's too many symbols for functions :smiley:
yeah, although I don't love the "higher-order functions work differently" exception, I do like the idea of having -> and => be the only two options more than I like the idea of having 3 different arrows
Why do you think it's important for a higher-order function to specify that it can work with effectful functions? It doesn't have to do anything different from the surface
if we go with => meaning "potentially effectful function" and -> meaning "pure function" , a neat thing that happens is that in TypeScript it's always =>, so for people coming to Roc from TypeScript, a learning guide can say "in Roc you can annotate the function just like in TypeScript, using =>, which means it might do effects...but now you can additionally annotate it using -> which guarantees it's pure"
I'm gonna re-skim the doc in case I'm missing something, but it feels like Task and ! might be simpler to understand over all this function coloring stuff.
That's a little inflammatory, actually. Take the above comment with a grain of salt.
Agus Zubiaga said:
Why do you think it's important for a higher-order function to specify that it can work with effectful functions? It doesn't have to do anything different from the surface
say I'm inside an effectful function and I'm trying to track down where a piece of outside state is changing (e.g. a database or file or something)
A thing I really value is being able to go through my calls one at a time, just glance at the type of the function being called, and say "that one is pure, so there is zero chance whatsoever that it's responsible for this effect I'm trying to track down, and I can move on to the next call without even glancing at that function's implementation"
for that to work, it has to be possible in all cases to look at a function's type signature, and only its type signature, and answer the question "is this function definitely pure or potentially effectful, meaning I need to investigate further?"
but it's not the higher-order function that isn't pure, it's the effectful function you passed to it, right?
I'm not sure how much Roc code will have a type signature written down because we provide such good type inference. If you're saying you'll use your editor to do this, can't we just give a code action to tell you where the effects are happening?
Agus Zubiaga said:
but it's not the higher-order function that isn't pure, it's the effectful function you passed to it, right?
it's both
like if I'm in the code, and I see a code to List.map foo bar
I may have gotten the effectful function bar passed in from elsewhere
I know what you mean, but wouldn't it already be obvious if in the same line you're passing an effectful function?
but passing around a function doesn't do any effects until some function actually cals it
I see
but if you're looking at the type of List.map, why wouldn't you look at the type of bar?
oh, I guess your argument is that you'd stop at List.map because you'd see that it doesn't have a ! or => in its annotation
well, that would be the most convenient - but in the design where higher-order functions are treated differently, if I'm aware of that rule I can know to look at the type of bar before deciding whether or not to move on
Ok, I see your argument now
I just think that in practice all higher-order functions will use => and then you'd have to check the passed functions anyway
totally
I think in general, a function being effect-polymorphic means you have to check it
and also in general, almost all higher-order functions will end up being effect-polymorphic
the only counterexample I could see to that is stuff like Parser
right, but then there isn't much benefit to having a separate syntax, is there?
because to be polymorphic, the effect type variable would have to appear in the type as a type parameter, and I don't think Parser a fx wouldn't be worth it just to be able to run effects in the middle of your parser (which...who actually wants to do that anyway?!)
Which means to get your benefit of needing less time to find an issue, we'd want to teach people to avoid notating functions as => where they want to avoid allowing effects.
That seems like something you may want in app code, but libraries should be discouraged from doing.
Agus Zubiaga said:
right, but then there isn't much benefit to having a separate syntax, is there?
to me, the main benefit of having a separate syntax for "equally effectful" is not to the functions which use it, but to the functions that don't use it
which higher-order functions would not use it?
so let's say I have this type:
... -> Str
because any other function is either effectful or not, and you can tell that from ! in the annotation
in the world where there's a separate "equally effectful" syntax, I'm like "that right there is a pure function, no further information needed"
there's no !, so it's defnitely pure
what if the full function type is this?
(Str -> Str) -> Str
so it's actually a higher-order function and therefore now potentially effectful if I pass it a higher-order function
so ... -> Str is no longer enough information to tell for sure that the function is pure
right, but if it's indeed effectful, the function where you call it would have to have a !
right, but I now always need to read the entire function type to be able to tell that
in the design where there is an "equally effectful syntax", ... -> Str is enough information. It does not matter at all what the arguments are
I can see that as a benefit, yes
in the design without it, I always have to look at the arguments no matter what, even if it's not higher-order
because I can't know if it's higher-order without looking at the arguments
so now I always need to consider all the arguments and the return type to know whether it's pure, no matter what
even if I never call a higher-order function in my entire life (somehow), I still need to check all the arguments every single time
because there's no other way to tell if they're higher-order
that's what I mean about the main benefit of having a separate "equally effective" syntax mainly benefiting functions which don't even use that syntax
the benefit to those functions is that I only have to look in one place to know that the function is pure, instead of always having to look at the args to figure out if it's higher-order or not, and therefore potentially only sometimes pure
I'm not saying that's a deal-breaker btw, I just want to make sure the downside is clear
Yeah, I can see how that helps
I'm still not sure how much difference it would make in the common case, but it's hard to know this until you have lived with the feature
Actually, question: with this design, I don't think whether a function is effectful can effect affect if it will fail early, right? It just works like a Result that might take a while to return, yes?
If an effectful function won't cause early returns in a way different from Results, then I really don't think there's a need for the above ability to go find where the failure happened
so what if we tried this as the design?
=> means there is definitely an effect-> with a visible function type (so, not hidden behind an alias or opaque type) is effect-polymorphic and will run an effect if and only if you pass it a function that runs effects (and otherwise it will be pure)-> in all other cases guarantees the function is pureso that means -> basically means "this can definitely be called from a pure function"
but if you call it from an effectful function, it might do an effect
"pure functions can always call -> functions and never => functions" is true in this design
Works for me!
Can you show the list.map type with that specification?
105 messages were moved here from #ideas > Purity Inference by Richard Feldman.
It would be nice to have a more effect-implying symbol for function signatures than =>, but I can't think of anything.
>> could work? It's probably fine
Brendan Hansknecht said:
Can you show the list.map type with that specification?
it's the same as today:
List.map : List a, (a -> b) -> List b
I think that is a bad idea
I think it hides effectfulness and that is just confusing
Cause that looks pure, but it isn't guaranteed to be.
It's as guaranteed pure as the function you pass
It does hide it, but doesn't not requiring ! hide it as well, in a way? I think this whole subject is a way to hide Tasks, basically
Especially given, it might be pure maybe (a -> b) is a deprecated argument and not actually called. Maybe it always returns an empty list
There is no guarantee here
So I think it really needs to be in the type especially as apis get more conplex
Sam Mohr said:
It does hide it, but doesn't not requiring
!hide it as well, in a way? I think this whole subject is a way to hide Tasks, basically
That is actually my biggest concern of this entire proposal. In most (all?) languages with async and await, await is required. This is for understandability. There is no reason it actually is needed in the code. Calling an async function is enough to know it must be awaited. Await could be hidden away.
I think it is probably a bad idea that roc is fully hiding away !. Roc doesn't require types. As such, I think it is rather important to have ! always be in the code for clarity.
I thought about requiring ! in expressions still, but it'd be awkward if you had to unwrap the Result too, you'd end up with a lot of !?
Easily telling if code is effectful or not is important. One of the biggest benefits of a pure language like roc. Really important to be able to see in the middle of a code function which class might be introducing impurity. In a long function seeing a File.read! is a reminder that that specific call might return varying results. Removing ! loses that and greatly hurts debugability
Brendan Hansknecht said:
Sam Mohr said:
It does hide it, but doesn't not requiring
!hide it as well, in a way? I think this whole subject is a way to hide Tasks, basicallyThat is actually my biggest concern of this entire proposal. In most (all?) languages with async and await, await is required. This is for understandability. There is no reason it actually is needed in the code. Calling an async function is enough to know it must be awaited. Await could be hidden away.
Go is a notable exception; all I/O in Go is async behind the scenes and there's no await keyword (or async keyword for that matter) and I've generally seen that described as a selling point of Go
That is not a correct view of go
And it is the reversw probalm
Async to sync is always safe. Sync to async is not
Brendan Hansknecht said:
Easily telling if code is effectful or not is important. One of the biggest benefits of a pure language like roc. Really important to be able to see in the middle of a code function which class might be introducing impurity. In a long function seeing a
File.read!is a reminder that that specific call might return varying results. Removing!loses that and greatly hurts debugability
a design we could consider is requiring ! at the end of the name if a function wants to do effects
Cause you can always make an async call and then block
That creates a sync api
I gotta go for a bit, but will check back in later!
I think this proposal gives us two things:
! in its signatureTask and normal values too.@Brendan Hansknecht I think you don't like the first point and its implications, which I'm also moving toward. But the second point isn't really possible in Roc, and would be nice to support as a way to make more code more useful.
Go everything is sync with explicit threading. The thread just may block on an async call (which makes it async)
I think something along the lines of List.map : List a, (a -> b ! e) -> List b would be nice
Yeah, I would be fine with 2. I would just require using ! with maybe effectful functions.
So at the very least, I presume you'd be okay with changing function signatures from Stdout.line: Str -> Task Str * to Stdout.line: Str -> Result! Str *?
Part of 1) above is hiding the concept of Task, which is separate from hiding effects
Richard Feldman [said]
a design we could consider is requiring
!at the end of the name if a function wants to do effects
I kind of like this, but I wonder if it goes against the goal of not bothering newcomers with the difference between effecfull/pure functions, because now they'll have to know when to put a bang in their function names and when not.
prior art in unison here https://www.unison-lang.org/docs/language-reference/abilities-and-ability-handlers/#abilities-in-function-types. they put effects “on the arrow” and a standard arrow is polymorphic on abilities. meaning higher order functions don’t need an ability and non-ability version (sorry for bringing up unison so often, not an expert but they seem to have solved some similar problems)
Jasper Woudenberg said:
Richard Feldman [said]
a design we could consider is requiring
!at the end of the name if a function wants to do effectsI kind of like this, but I wonder if it goes against the goal of not bothering newcomers with the difference between effecfull/pure functions, because now they'll have to know when to put a bang in their function names and when not.
Come to think of it, maybe it's a benefit, the learning curve passing through the difference between pure/effectull functions before types.
drew said:
prior art in unison here https://www.unison-lang.org/docs/language-reference/abilities-and-ability-handlers/#abilities-in-function-types. they put effects “on the arrow” and a standard arrow is polymorphic on abilities. meaning higher order functions don’t need an ability and non-ability version (sorry for bringing up unison so often, not an expert but they seem to have solved some similar problems)
Here's the map signature, FYI https://share.unison-lang.org/@unison/base/code/releases/3.17.0/latest/terms/@ko93h54ensirkthhekqb7898lk1j1klhv5mplfpcui1bh03dcrg9i2tp3ibfk3r1qp36uhie79el2fbc15rpm6bkgl7l1tfahst2m6g. the {e} doesn't need to be explicitly written
example usage https://gist.github.com/drewolson/14c36dca67c4b59b05d1099f6df45bf5
Is the {e} required for all higher-order functions?
Or passed functions, I guess
That might be a good compromise. All passed functions need either the "equally effectful" syntax or the effectful polymorphism variable
so you can't forget to do it and you can always pass an effectful function to a higher-order one
a -> b is syntactic sugar for a ->{e} b
yes, equally effectful is right
We could allow that, but then Rich's point of "I want to be able to read the signature and know what's happening"
yeah, true, it definitely doesn't help there at all
I'd rather some concise but explicit way to do it
I think at least I prefer a ->{e} b over a -> b ! e because it has that "during" implication to it
drew said:
prior art in unison here https://www.unison-lang.org/docs/language-reference/abilities-and-ability-handlers/#abilities-in-function-types. they put effects “on the arrow” and a standard arrow is polymorphic on abilities. meaning higher order functions don’t need an ability and non-ability version (sorry for bringing up unison so often, not an expert but they seem to have solved some similar problems)
unless I'm missing something, it sounds like Unison has this design when it comes to -> (although they don't have => because they enumerate which effects a function does, so a generic "this does effects" wouldn't make sense for them):
Richard Feldman said:
so what if we tried this as the design?
=>means there is definitely an effect->with a visible function type (so, not hidden behind an alias or opaque type) is effect-polymorphic and will run an effect if and only if you pass it a function that runs effects (and otherwise it will be pure)->in all other cases guarantees the function is pure
looks like it
I guess an alternative way to say it is:
=> means there is definitely an effect-> means "equally effective"-> in the function type, the function must be purewhen I say it that way I like it better
feels a lot simpler to look at a function and tell if it's pure
"is there one ->? if so, it's definitely pure"
It would make a lot of sense if the syntax for the effectful type var was a b> c, but I guess that looks weird :upside_down:
(where b is the "effectfulness")
So now this seems like a good solution for making the default function signature -> effect agnostic while still allowing explicit annotation via =>. Is there a way to make Brendan happy w.r.t. obvious await via !?
to explore the "-> and => only, no ! in types" design further - in that world, what would the syntax be for when there's a type variable involved?
-a-> :stuck_out_tongue:
Like actually
yeah I meant that as a joke but now that I see it I actually kinda dig it :big_smile:
it's pretty self-explanatory haha
If you're not a fan of >2 chars long operators, then I see why it's off the table
in general I don't like them, but this feels different
we can call it the brochette operator
like splitting up a 2-char operator
Something you were getting at with the "I want to look at the ... -> Str part of the function is the localization of useful info
I think that -a-> localizes the effect info to where it's happening
I've already been pushing this idea, so I probably sound like a broken record, but having stuff by the arrow Just Makes Sense to me
I think our options are:
a -e-> ba -> b ! ea ->{e} ba -> b {e}my preference: 1 > 3 > 2 > 4
Literally same
I think the ! makes sense if we keep the ! suffix for awaiting effectful code, but is vestigially the only part of Roc with ! if we get rid of the !
The terse options (a.k.a. 1 and 3) seem less nice if we have long effectful type variables: -effect-> is still legible, but not as nice as -e-> IMO.
But I still think it's obvious what it means: stuff that happens "during" execution
so putting together the most recent discussed design ideas:
List.first : List elem -> Result elem [ListWasEmpty]
List.map : List a, (a -> b) -> List b
# alternatively, if you want to write out the variables:
List.map : List a, (a -fx-> b) -fx-> List b
File.readUtf8 : Str => Result Str [FileReadErr File.ReadErr]
Looks GREAT to me! :heart_eyes:
Yeah, I really like this!
notable features of this design:
! in types, which means no need to explain why foo : Str! is invalid; you can't even attempt to write that-> functions and can never call => functions-> means 'equal effectfulness', which implies that if there's only one ->, the function is pure"I guess the thing that I'm not sure that you, Rich, are convinced of, is whether -> inferred to mean -fx-> is good with you. And if you're tentatively agreed pending code examples, what condition would cause us to change the -> behavior?
I'm actually good with it now
the new wording of the rule (which is technically slightly different semantically than the old one, but not in a way that I think is likely to matter ~ever) seems much better to me
there's less thinking involved because you can just scan the type to count the number of ->s
Great!
I don't know if you saw but a compomise I proposed is to always require -fx-> for passed functions
put another way, it doesn't have ... -> Str but it does have ... -> ... -> Str meaning it's effect-polymorphic
Agus Zubiaga said:
I don't know if you saw but a compomise I proposed is to always require
-fx->for passed functions
I'd rather not haha
I think higher-order functions are so common, it's worth not having to put extra variables in them
especially because they look complicated enough just from being higher-order
without having to make them even longer
yeah, makes sense
that's what I prefer too
Richard Feldman said:
put another way, it doesn't have
... -> Strbut it does have... -> ... -> Strmeaning it's effect-polymorphic
maybe another way to say what I like about this rule is that it feels like "-> means equality of effects, and then there's a natural special-case that falls out of that where functions are pure" is nicer than "-> normally means pure EXCEPT! there's a gotcha to be aware of where sometimes it doesn't, which we introduced for convenience"
even if in practice they have almost exactly the same semantics :big_smile:
Wait a second: maybe I'm crazy, but how to we handle non-function Tasks??
If my code is
main : Result {} *
main =
Stdout.line "Hello, world!"
Where do I put my effect annotation?
Crazy idea, but between this issue and the inability to specialize non-function values causing issues in our platform linking, maybe we just ban Tasks that aren't returned by functions?
This might need a new topic...
That's not possible, you have to make it a thunk
I mean the proposal specifies only functions can be effectful
Well currently the actual type of main in basic-cli is Task I32 _, not {} -> Task I32 {}
yeah, it'd have to become a function
Okay, so yeah, we're banning bare Tasks, good to be on the same page
the doc has a section titled "Thunks" that talks about this a bit
Okay, reading
One piece I don't get about the current proposal to have -> ambiguous over effectfulness.
If I see:
Str -> Str couldn't it do an effect? Like it could use the first Str to call file.ReadUtf8?
Feels like I would never know if a function is pure
Also, I feel like sometime you don't want to allow any sort of effects in your higher order functions (though maybe that is a wrong opinion...not sure)
Brendan Hansknecht said:
One piece I don't get about the current proposal to have
->ambiguous over effectfulness.If I see:
Str -> Strcouldn't it do an effect? Like it could use the first Str to call file.ReadUtf8?
the most recent idea is to have all ->s in a type signature share an effect variable
so if there's only one -> in the type then it's definitely pure
Brendan Hansknecht said:
One piece I don't get about the current proposal to have
->ambiguous over effectfulness.If I see:
Str -> Strcouldn't it do an effect? Like it could use the first Str to call file.ReadUtf8?
From Rich's comment, "-> means equality of effects".
Meaning if I have a function List.map : List a, (a -> b) -> List b, then we inherit all effectfulness from our inputted callbacks. If the a -> b mapper callback is effectful, it "pollutes" all -> arrows and now the parent function is effectful. If there's only one -> like in Str -> Str, then it can't be "polluted" by any effectful callbacks
I don't think that follows from your first statement, So Str -> Str would be Str -fx> Str and fx could be effectful.
I think this is definitely an inconsistency
to demonstrate why it's not, how would you implement an effectful version of that function?
like that's the type, what's the body?
I think you have two sources for effectfulness: the body of the function, and callbacks provided to the function. If the body is effectful, we infer the whole function as =>. If the callbacks are potentially effectful, we get -fx->.
In Str -> Str, if the body is effectful, we'd infer Str => Str, if it isn't, we infer Str -> Str.
(fx is * in Str -> Str)
And if you annotate Str => Str as Str -> Str, you'd get a type error
yeah so if you have foo : Str -> Str I claim it's impossible to write an implementation of foo that does effects :big_smile:
(because if you call an effect, that function's effect variable would have to become bound, and therefore its inferred type would have to be Str => Str, and Str -> Str would be too general)
The only way to do it would be to write a stdlib function with effects lol
I don't think I follow:
x : Str -fx> Str
x = \str ->
file.readUtf8 str
fx is parametric and roc would infer that fx must be effectful due to the call to file.readUtf8
Str -fx-> Str is like saying Str -*-> Str
that type is incorrectly claiming that the function is more flexible than its implementation is
This still doesn't sit right
x : Str -fx> Str
x = \str ->
str
If that is the case, this is also too general
Cause it is forcing fx to be no effects
that's the point though!
Which also is more general than being unsure about effects or not
so if I have a function that does effects, I can call both pure functions and effectful functions from it
Like both specifically effectful and specifically pure are not as general as maybe effectful
the way that works (using type unification) is that pure functions have an unbound effect variable, and effectful ones have it bound (to {} or something, doesn't really matter what as long as it's concrete)
so actually a pure function is as general as "maybe effectful" because otherwise you couldn't call a pure function from an effectful function
which of course wouldn't be desirable :sweat_smile:
Let's say I have this function:
doubleMap : a, (a -fx-> b), (b -fx-> c) -fx-> c
doubleMap = \a, mapToB, mapToC ->
a
|> mapToB
|> mapToC
If mapToB is not effectful but mapToC is, then effects are being done, so the three fx vars unify to "effectful"
Ok, I guess I get it. Only works cause pure implicitly can convert to effectful
i still don’t understand why => is required in this proposal, but honestly i’m just a fly on the wall here
the most recent idea has => meaning what ! did in the doc
so in this design, => means "definitely effectful"
I'll make a v2 of the proposal at some point based on the discussions here
two slight tweaks I just realized are necessary
one, similarly to how we elide unbound type variables in open tag unions, we need to do the same for unbound effect variables in type aliases and opaque types, because otherwise you couldn't do this:
Foo := (Str -> Str)
it would have to be
Foo a := (Str -a-> Str)
which would be really annoying
second, and related, we should say that -> is sugar for -fx-> across an entire function annotation, not across an entire type annotation
otherwise this wouldn't work either:
Parser a := [
Succeed a,
Foo (Parser a -> Parser a),
Bar (Parser a -> Parser a),
]
it would have to be:
Parser a fx := [
Succeed a,
Foo (Parser a -fx> Parser a),
Bar (Parser a -fx> Parser a),
]
which again would be undesirable :big_smile:
actually that second one might not be necessary, if we wanted to allow hiding the effect variable in general - even if it's bound :thinking:
I don't think we have a precedent for that, but a variable is a variable...maybe it's fine
I think this is my exact concern. This really is losing the ability in roc to tell what is pure or not
how so? In the tweak I suggested, these are 100% definitely both pure:
Parser a := [
Succeed a,
Foo (Parser a -> Parser a),
Bar (Parser a -> Parser a),
]
because the -> sharing variables only applies within a function definition, not the surrounding tag union
Foo := (Str -> Str)
... I thought it was pure and safe to use here without magical changes, but actually it ran an effect and changed a bunch of my code to effectful
Becuase I used foo which I thought could only wrap a pure function, it lead to weird bugs due to expecting purity that wasn't there.
oh, definitely if you don't put a type variable in Foo you wouldn't be able to do that
(I should have mentioned that explicitly earlier!)
List.fooStrings : List Str, Foo -> List Str
If Foo contains effectful code, then this is now effectful even with only one visible ->. That is a issue, yeah
like Foo fx := (Str -fx-> Str) would let you put an effectful function in there
but Foo := (Str -> Str) would only accept pure functions
Richard Feldman said:
it would have to be
Foo a := (Str -a-> Str)which would be really annoying
I guess I misunderstood :point_up:. I thought you were saying that you wanted this to work implicitly causing adding the type variable for effectfulness would be annoying.
oh no, sorry haha
I just meant that you should be able to put pure functions in type aliases and opaque types (like today) without adding a type parameter to them
(and if you want it to be polymorphic, also like today, you do have to add a type parameter)
Even if we force type aliases/opaque types to annotate their effect-polymorphism, it's still
List.fooStrings : List Str, Foo fx -> List Str
which isn't visibly effect-polymorphic unless the arrow is changed to -fx->
We could do some analysis during type checking to say that if you reference a type that is effect-polymorphic, then you need to explicitly annotate your return arrow as effect-polymorphic?
This seems better than being allowed to write code that "looks pure" (meaning has one arrow), or not doing said analysis and then needing to know things about all used aliases
I found a simple example that gets really weird:
memoize: Dict a b, (a -> b) -> b
Suddenly memoize doesn't work as expected
Cause a -> b is not a pure function
So memoization is not actually safe, but the writer assumed only pure functions could be passed in and that is really what the api seems to suggest
hm, can we find another example? I don't think memoize with that type can do what it says it would :sweat_smile:
I guess it could be this:
memoize : Dict a b, (a -> b) -> (Dict a b, b)
Sorry:
memoize: Dict a b, a, (a -> b) -> (Dict a b, b)
yeah something like that
right, so yeah you'd want to be able to say "this function has to be pure"
That’s also unsafe because you could pass different functions in later calls
so this would work I think:
memoize : Dict a b, a, (a -x-> b) -y-> (Dict a b, b)
(or this, but I'm trying to avoid using * since we might remove it)
memoize : Dict a b, a, (a -*-> b) -*-> (Dict a b, b)
I thought about that but would that compile?
Well that is the issue, I think the problem is that you have to specify that. You could also just use -*>
I think you couldn’t call the function inside
Because the type says it might return an effect, but you cannot return that effect from the higher order function
hm, I don't follow
I don’t follow myself actually :sweat_smile:
I think -*> works, but I think the issue is that a user has to specify it.
It means that the users has to definsively program. Any function they take in as an input by default might be impure
I have to always assume impurity in any code I look at online with higher order functions. This is bad
if we had a separate syntax it would still have to be chosen defensively
well this is the entire goal though, right?
like we can't possibly have it both ways
either by default higher-order functions are effect-polymorphic or they aren't
I think the original => was a lot better
It at least was opt in
but people would still use it for every higher order function
I don't think so
so they would have to go outside the norm to enforce pureness
I think most people will use the default of -> and never think about it.
I don't think that would last
if your package offers higher-order functions that aren't effect-polymorphic (like the builtins would be), people would complain and ask you to change it
This hypothesis stuns me
So we need at least an opt-out
Imagine a world where we don't have this change
Most higher order functions would not use tasks, would they?
And if my function doesn't return a task, I can't call an inter function that returns a task
probably not, but I think the main reason for that is that people don't want to duplicate all their higher-order functions and name them ___Task :big_smile:
The only minor exception to that is where a user has control of the state
I think most higher order functions never have a need to be impure
Also, in the Task world you can still use walk-like functions to compose them
well that's still true
just do it with thunks instead of tasks and then run all the thunks
Only by passing effectul functions, right?
you can always build up as many thunks as you want using pure functions
doesn't matter if the thunks are effectful or not
defining a function is pure
only running it can possibly do effects
Yeah, but you cannot compose them with a function that disallows effects
like for example, today I can do List.map strings File.readUtf8 and I get back a List (Task Str _)
and I can then walk that list to run all the tasks
similarly, in the effect world I can do List.map strings \str -> \{} -> File.readUtf8 str
You cannot run from inside List.walk though
Unless it allows effectful functions
I mean even if that wasn't allowed in this world, you can still recurse
and pull them out of the list one at a time if you have to
Like with a recursive function?
That works for list but wouldn’t work with opaque types from a package
ok but if that exposes a pure walk function I can use that to obtain a List and recurse etc.
and even if List didn't support that, you could build your own cons list out of tag unions
there is no way to prevent people from using pure functions to assemble sequences of effectful thunks and then run them, I promise :big_smile:
it's just a matter of how convenient that is
Haha yeah, I know
With this prososal, why are we keeping roc pure at all?
It doesn't make sense to me. Roc is a functional language. In roc, higher order functions are super common and all over the place. If every higher order function is ambiguous to purity by default, what are the benefits of having a pure language at at all? Almost all functions would be maybe impure.
It’s just mean it’s more ergonomic in the Task world
Brendan Hansknecht said:
With this prososal, why are we keeping roc pure at all?
It doesn't make sense to me. Roc is a functional language. In roc, higher order functions are super common and all over the place. If every higher order function is ambiguous to purity by default, what are the benefits of having a pure language at at all? Almost all functions would be maybe impure.
oh I don't think so - I expect the percentage of pure functions in a Roc program to be unchanged by this
Please explain, I definitely do not understand.
To me this definitely feels like so much convenience and implicitness such that the langauge may as well be impure.
Like I will have to be just as defensive as a programmer using an impure language cause I use higher order functions all over the place and any of them could be impure now.
oh not at all
like ok so let's say I write a function with this type:
foo : Str -> Str
foo = \str ->
in an imperative language, the body of this function could be doing effects all over the place
there's no version of this proposal where that's true
100% for sure, calling foo will perform zero effects
Brendan, you are probably already thinking this, but I think the goal here is to think like a startup: let's try something and be idealistic in our design, and if it works out, cool, if not, we move on. We have to be a little "unreasonable" to get outside of our comfort zone, but that doesn't mean we'll stay in crazy land if we can't make it work, all said and done.
and if foo tries to do any effects, either by calling a higher-order function or not, it will result in a compile error
Yep. But in practice, I think many of my functions are higher order.
ok so let's pick one of those higher-order functions!
let's say I am authoring this higher-order function:
bar : (Str -> Str) -> Str
bar = ...
100% for sure, the implementation of bar does not call any effectful functions
I can tell you that just from the type
otherwise it would be a compile error
just like today, no different at all
because if it did call any, the type would have to change =>
the only difference is that now bar can be passed an effectful function, and if so, then calling bar becomes an effect
but you could only possibly do that from a function that was already effectful anyway
just like today
like I couldn't call bar from foo unless I passed it a pure function
in which case no effects would be performed
to me, it is absolutely essential to preserve that property - that is the main benefit of pure functional programming to me
is that I can rule things out by looking at a function's type and knowing its purity based on that
and the "polymorphic effects" part of this doesn't change that meaningfully
because I can already call List.map things \thing -> File.readUtf8
and that's pure because all it's doing is building up a list of tasks, not actually running them
I can only run them from within a Task
and similarly, if I'm inside a pure function in this design, I can only build up a list of effectful thunks using List.map, I can't actually perform any effects except from within an effectful function
that's why I don't think the percentage of pure code would change
if I tried to introduce an effect somewhere, it would cause exactly the same cascading ripple of type changes as today
except instead of all those functions having to change to Task, they'd change to =>
that's the only difference
But if bar calls the effectful function passed into it twice (assuming it is cheap to run and not effectful), it could get two different results
I'm not sure if there is no difference. If I wrote this code:
main = \{} ->
List.range { start: At 1, end: At 5 }
|> List.map \num ->
Stdout.line "now is $(Num.toStr num)"
Sleep.millis 1000
Stdout.line "done!"
Would "done!" print before or after my count? Would the count take 5 seconds to run, or 1 second followed by 5 simultaneous prints?
So bar needs to be written defensively assuming that the function passed in might be effectful and is only valid to call once with the same inputs
Like a compare function could return different results on every call
hm, I see your point but I have a hard time imagining the bug report count resulting from people not even thinking about that being more than a rounding error
like if I can't call effectful functions directly, why would I call the same function twice?
that's just worse than calling it once and caching the answer
I think if I have assumed cheap higher order function, I would call it multiple times without worrying about it.
That doesn't feel far-fetched
I mean it's definitely possible, I just don't see it happening a significant amount of the time in practice
This is reminding me of Koka's caveats around resumption. You don't know if effect handlers will return zero times, once, or many times. Because effect handlers do whatever they want, you have to plan around them.
And here, so long as code is effect-polymorphic, you can't write code assuming it has consistent output, yes.
It might even be better in some cases due to tradeoffs between compute and memory or due to a change that might happen but isn't guaranteed
I'm mostly trying to point out that this opens the door to those pains
They may be rare, but they are definitely a new kind of pain that didn't exist before and also is not captured by the type system
Andrew Kelley has some perf-based videos around how much faster computation is over memory storage/retrieval
I appreciate that
If I was going to run an effect I might place it in a different location in my function where it is best for a potential async suspensión to happen
but I think it's also fair that, to Agus's point, for this to happen kinda requires someone not to be thinking about this consideration and going through the motions without realizing the implications of effect-polymorphism
Otherwise, the effect may capture a metric ton of intermediate data (and that can be super expensive as seen by problems with roc-wasm4)
and even if we make it opt-in, I think it will be so common for higher-order functions to do this that the "I'm not thinking about it and just going through the motions" reflex will be to annotate it as effect-polymorphic anyway
although to be fair, if the function wasn't higher-order and then became higher-order, and that required changing syntax, that might naturally give someone pause as they go to change the syntax
to ponder the implications
but now we're talking about an even smaller delta of outcomes as the conditions for what sequence of unusual events has to transpire for a bug to occur gets longer and longer :sweat_smile:
incidentally, one of the other reasons I don't think this would make a difference in the proportion of pure functions people write is that in Clojure and OCaml you can just run side effects from any function, and it's still a very strong cultural norm to write as many pure functions as possible
in both communities
so we're talking about having a language where not only would that cultural norm be at least as strong as it would be there, but also changing from pure to effectful requires going around and changing all the types of all the callers (just like today)
Is there some way that we could estimate how much people will want the average higher-order function to be effect-polymorphic vs pure?
It seems like if it was (hyperbolically) 99% of people, then it definitely should be the default, and it wouldn't be worth making people always opt-in when it really is the default.
But if it's 1% of people, then it should be opt-in, and then I think Brendan's issue goes away.
But we are talking from different realities here, because we seem to disagree on what that number is, and therefore what our design goals should be.
and I've never heard about a problem of people in Clojure or OCaml accidentally calling things in higher-order functions multiple times and then having effects get repeated, even though that's always been possible with all higher-order functions in those languages
I guess Unison people might have even more apples-to-apples data on that, since Unison literally has the higher-order function design we're currently discussing :big_smile:
Good suggestion! I'll see if I can find anything.
Just curious, Rich(ard) [do you have a preference?], if it really was that most people wanted to pass pure functions to libraries, and only rarely pass effectful functions, do you think you would want the -> to default to pure, and require annotation to become effect-polymorphic?
it's a good question
so one possibility I hadn't considered: what if we went with the design where you have to use explicit -fx-> and we just didn't make most higher-order functions effect-polymorphic
like for example, give it to List.walk but not to List.map
so then the List.map signature is the same as today because it works exactly the same way as today
that design definitely has the simplest rule set, which I appreciate
-> means it's pure=> means it's effectful-var-> means it's polymorphicthat's it
Which is to say, make library authors have to decide when to opt-in for effect polymorphism?
and if in practice people weren't going to use List.map for effects except in very rare cases anyway, are we really missing out?
Yeah, I think this would be a good compromise as I mentioned before
I agree that List.map isn't a function that people would really use to perform effects with, but it does mean that authors might get pestered to make their higher-order functions more flexible.
I would expect a lot of packages to default to effect polymorphic higher order functions just in case
But like the Unicode situation with Roc, some things really do need proper consideration, this might just be one of those
Agus Zubiaga said:
I would probably still expect a lot of packages to still default to effect polymorphic higher order functions just in case
I actually predict most of them will follow the example of the stdlib, and follow whatever convention is established there
e.g. if the convention is that you make walk polymorphic and that's it, I'd expect libraries to do that too
Good point
this direction is pretty interesting
We should recommend making package functions effect-polymorphic unless there's a good reason not to in our tutorial
Some functions are more likely to need effects than other
Yep
like I think an interesting observation is that today, there's no real demand for List.mapTask but I've wanted List.walkTask on several occasions
(but that hasn't been implementable because Task wasn't a builtin)
Most data structure packages could allow effects in their most powerful function and default to pure for the rest
One example to consider is List.sortWith : List a, (a, a -> [LT, Eq, GT]) -> List a. Maybe that function wants to be always pure instead of polymorphic. It doesn't make a lot of sense to pass a non-deterministic comparison function, so you're not losing a lot by making it always pure, and one downside of having it be effect polymorphic is it means that changing the sorting algorithm is easily observable rather than just an implementation detail. (Although I guess that's true either way because ill-behaved pure comparison functions can probe the sorting algorithm too)
another thing that appeals to me about this direction is that it simplifies the decision space
like I don't have to think "hm, is there a way I could write this effectful thing using effect-polymorphic List.map in a cool way?"
it's just like "nope that only works with pure functions, ok one less possibility to consider among N equivalent possible ways to write it"
(which is the corresponding benefit to less flexibility when the flexibility is a convenience)
@timotree the example Brendan gave earlier was a memoizing function. I think the same thing applies here, we have cases where we want to ensure we're not running someone else's effectful code.
at the risk of opening up a can of worms (but it is very relevant to this discussion I think) I have a WIP proposal that would let you do a for loop style based on giving it a walk function
and in that world, if walk is effect-polymorphic, you could use the for syntax for a lot more things without having to call walk directly
I'll avoid delving into that then, but yes, that's where walk comes in handy, and map doesn't
which in turn would mean even less demand for using effects in functions like List.map (which are already convenience wrappers around walk) because the main point of for is to be an ergonomic wrapper around walk :big_smile:
Richard Feldman said:
so one possibility I hadn't considered: what if we went with the design where you have to use explicit
-fx->and we just didn't make most higher-order functions effect-polymorphic
[...]
Richard Feldman said:
->means it's pure=>means it's effectful-var->means it's polymorphic
@Brendan Hansknecht when you get back, I'm curious what you think of this idea
This is definitely my favorite of the proposed directions!
yeah it's the new frontrunner for me too
Works for me!
are there many effect types in roc or only one?
Just the one (for now)
as the person who initially offered the prior art, i will say this all does feel quite complicated :sweat_smile:
haskell works without effect polymorphic functions with foldM basically
Unfortunately for us, this is an intrinsically complex domain, so Roc's goal to manage complexity while still making it known what's happening is just hard
very true
FP has more ornate solutions that we are forgoing because we don't want to allow wizardry
Richard Feldman said:
->means it's pure=>means it's effectful-var->means it's polymorphic
I am much happier with that. I still am nervous around it only being represented in the type, but I have no quals with seeing what it feels like to only have it in the type.
drew said:
haskell works without effect polymorphic functions with foldM basically
that's essentially the same idea as "make walk effect-polymorphic and all the other ones pure unless there's a specific reason not to (e.g. parallel should be effect-polymorphic too)"
i think it’s more the walkTask equivalent
haskell has a bunch of *M variants
that's what I mean, yeah
but i guess higher kinded types means only one variant forever
haskell works without effect polymorphic functions with foldM basically
I think this proposal could make working with effects significantly nicer than it is in Haskell
foldM being equivalent to either walkTask or effect-polymorphic walk
yep yep
except foldM can’t take a pure function
you need Identity nonsense
i guess foldM is polymorphic over a higher-kinded effect, but it must exist, whereas ->{e} in unison is polymorphic over a potentially empty set of effects
like ->{e} could actually mean ->{e1,e2,...} which i suppose makes this latest suggestion forwards compatible with an N-effect roc world
speaking of which, does => become useless when the number of effects becomes greater than one?
is => simply syntactic sugar for -Task->
I don't think there is any plan to add any effects other than Task
if that's the case, -?-> could be "effect or no effect"
though it is annoying to type :smile:
Hey all - sorry, unfortunately I don't have the bandwidth to read everything in this thread, keep up the great energy :) I just want to add one perspective to this that I've talked to Richard about before. Some of it may already be in the thread, again, sorry that I haven't read everything.
To whatever extent possible, I would really encourage avoiding any polymorphism in effects in the syntax visible to the language user. Obviously effect polymorphism is necessary, but I believe there are approaches here that can hide that entirely from the syntax/having to think about it most of the time.
More polymorphism means more things to think about. If author must now always think about whether their function is effect polymorphic or not, I wonder if that ends up putting the language in the same spot it started with - the dichotomy of task/not task is removed and so is some syntax/types, sure, but it's been traded it for this other thing.
Roc's type system can be difficult for some to get started with because it is novel/unfamiliar/complex in some ways (think of open tag unions, polymorphic unions, and optional record fields). To whatever extent possible, I think it is prudent to avoid adding another dimension unless absolutely necessary. It also opens the gate to ecosystem splits like a fully effect-polymorphic/rigidly pure ecosystem, even if the sight of that future isn't visible now.
If the goal of polymorphism is to gain insight into what functions are definitely pure, I would consider focusing on other ways to surface that specifically. For example, from my perspective the benefit of that goal is that when I am debugging function X, I know I only need to look at y_1, ... y_n function calls. If that's the case, what if effectful calls are signified at the call site instead of the declaration site? So for example, List.map ls f is List.map! ls f if f is effectful. Then there is no need for polymorphism in the declarations in the context of this use case specifically. Not saying that's definitely the way, but pushing really hard on avoiding type system complexity via polymorphism is a very valuable metric, if it can be achieved.
Ayaz Hafiz said:
I just want to add one perspective to this that I've talked to Richard about before.
for context, and credit where due, it was a previous conversation with Ayaz where he advocated for reconsidering algebraic effects that led to this proposal in the first place!
Ayaz Hafiz said:
So for example,
List.map ls fisList.map! ls fif f is effectful.
Yeah, this honestly makes a ton of sense to me.
it's also pretty clear at the call site
i also think if there's ~no chance of having more than 1 effect, maybe *Task functions are fine? They are certainly explict.
anyway, curious what you all come up with!
some brief thoughts on some of these points:
Ayaz Hafiz said:
To whatever extent possible, I would really encourage avoiding any polymorphism in effects in the syntax visible to the language user. Obviously effect polymorphism is necessary, but I believe there are approaches here that can hide that entirely from the syntax/having to think about it most of the time.
I think if it's only visible in the types of e.g. walk and parallel, and walk ends up being primarily used via for syntax sugar, then people will encounter the types extremely rarely. So I don't think in that design it's something people would be thinking about a lot, but I could be wrong!
Ayaz Hafiz said:
More polymorphism means more things to think about. If author must now always think about whether their function is effect polymorphic or not, I wonder if that ends up putting the language in the same spot it started with - the dichotomy of task/not task is removed and so is some syntax/types, sure, but it's been traded it for this other thing.
Regarding "More polymorphism means more things to think about. If author must now always think about whether their function is effect polymorphic or not..." - I think this cuts both ways!
As noted earlier, if (for example) List.map and friends are all effect-polymorphic, then as a non-library author I'm thinking about a much larger number of ways to express the same thing...and there are a lot more library users than authors! Also, I predict authors will follow the precedent the stdlib sets (libraries being "idiomatic" is generally considered a good thing in most languages), which means I don't think they'll need to think about it all that much on a per-function basis.
I do think this is a reasonable consideration, but in general I don't think we can get away with "somebody is thinking about effect polymorphism" if it's a thing that exists at all.
Ayaz Hafiz said:
Roc's type system can be difficult for some to get started with because it is novel/unfamiliar/complex in some ways (think of open tag unions, polymorphic unions, and optional record fields). To whatever extent possible, I think it is prudent to avoid adding another dimension unless absolutely necessary.
I think both tag unions and record fields are interesting related examples here, in that:
walk actually do so in practice).Ayaz Hafiz said:
It also opens the gate to ecosystem splits like a fully effect-polymorphic/rigidly pure ecosystem, even if the sight of that future isn't visible now.
I think that is a totally valid concern, although I just don't think the demand for it would be high enough. The point that started us down the path that led to the current design idea was that in practice it would probably be the case that even if List.map supported being passed an effectful function, 99+% of the time people would pass a pure function to it anyway.
if that hypothesis is correct, and there isn't that much demand for effect-polymorphic higher-order functions outside of a few outliers like walk and parallel, then it stands to reason that there wouldn't be much demand for a whole alternative effect-polymorphic ecosystem either
Ayaz Hafiz said:
what if effectful calls are signified at the call site instead of the declaration site? So for example,
List.map ls fisList.map! ls fif f is effectful. Then there is no need for polymorphism in the declarations in the context of this use case specifically. Not saying that's definitely the way, but pushing really hard on avoiding type system complexity via polymorphism is a very valuable metric, if it can be achieved.
this is an interesting idea, although a notable nice feature of the current design idea is that it's sort of the bare minimum
like no matter how the design ends up evolving over time, the minimum starting point has to include:
every other idea builds on this foundation, and the current frontrunner design is basically just "ship this foundation"
and maybe it ends up being that in practice we don't like it and want to change things, but given that we have good reason to suspect this is the nicest design overall long-term anyway (and it certainly seems like the simplest one we've discussed), I think there's a good case to be made that it's the most logical starting point
and then we can see how these various hypotheses play out in practice and discuss what changes to make (if any) based on seeing what the experience of using it is actually like in practice
So for example,
List.map ls fisList.map! ls fif f is effectful.
This is definitely still my preference for all effectful functions. Just require the ! still like other languages would require await. Makes it visible at the callsite to the end user.
even if
List.mapsupported being passed an effectful function, 99+% of the time people would pass a pure function to it anyway.
I still think it would be reasonably common to use it for effects. Specifically for things where you deal with querying websites. Being able to run a list of queries, and get a list of results sounds like it would be wanted. Though that would probably be List.mapTry to be fair. Like a webscaper that grabs n pages. Loads there links into a new list and the loops.
Oh, nvm, I see what you mean. You would want parallel to run them all at the same time. Not map.... So map would be pretty rare due to the thread limitation.
But I still could see lists of tasks to run in general in some cases.
a syntax for defining effect-polymorphic function types with a named type variable
I don't think this is required.
If a function is effectful, you must use ! to call it.
If a function is maybe effectful and any of its args are effectful, you must use ! to call it.
Otherwise, everything must be pure and it is called normally.
This would mean that a function taking in a higher order function would not know if that function is pure or not (which I'm not the biggest fan of), but it would mean that the call site for any effectful direct function would use !. It also forces simplicity in the type system but has some things that are impossible to expresss where it would treat something as effectful even though it may not truly be effectful. But that is technically valid cause all pure functions can implicitly be converted to effectful.
Not saying :point_up: is the correct solution, but I think that constraint may be designing ourselves into extra complexity that isn't actually required. We can force simplicity if we accept some restrictions.
Another thougth with regards to removing effect-polymorphic function notation (i.e. -var-> syntax, in the latest proposal):
As I understand it, we'd only need that when a single type signature might contain multiple type-polymorphic functions, like in this example:
fn : (a -e-> b), (c -f-> d) -> { one: a -e-> b, two: c -f-> d }
I'm curious if we can come up with real-ish examples of library functions that might want to use this syntax, to get a sense of what we'd lose if the language didn't support those types of functions.
Because similar to how the the single-implicit-effect-parameter in => allows us to combine what would otherwise be an effectful and non-effectful version of List.walk into one function, I would imagine a function with multiple explicit effect-type-parameters can be replaced with a couple of different functions with that have at most one polymorphic effect parameter each. And if that avoids a very complicated type, maybe it's the better design?
Oh wait, the problem is you can write a function fn = \one, two -> { one, two }, the compiler might infer the complicated type, and if it's illegal we'd need to explain folks why their seemingly reasonable function is not allowed :thinking:
(just parroting what Richard already said before)
yeah the variable name has to exist, at a minimum
the question is whether we add optional syntax sugar in addition to it :big_smile:
regarding ! in expressions, one thing we could try is making them optional in the initial release
I know the whole point would be to make them mandatory, but having them be optional would let us try out both styles and see how they feel in practice
like for example we can see how often !? comes up and how it feels, and we can also separately try out implementations with no ! in the function body and see how that feels too, and compare them
i'm assuming you all have already read this, but the frank paper seems like good prior art as well (https://arxiv.org/abs/1611.09259)
most notably, from the introduction:
- a novel approach to effect polymorphism which avoids men- tioning effect variables in source code, crucially relying on the observation that one must always instantiate the effects of an operator being applied with the ambient ability, that is, pre- cisely those algebraic effects permitted by the current typing context;
I think this is what Unison's system is based on
yep, correct, though frank seems to make some different decisions in the paper
and it's boiled down
it seems that the concept of "suspension" is deeply built into both langauges, i'm not sure how isomorphic this is to Task
+1 on the frank paper, it is excellent foundational material I would encourage reading, as well as the Leijen and Xie papers on effects in Koka, and https://dl.acm.org/doi/pdf/10.1145/3485479 and https://arxiv.org/pdf/1812.11664. The last one shows a rigorous effect system in OCaml without type extensions - OCaml today also has effects without having effect polymorphism (or even the idea of effects) in the type system (besides some minor things for continuations which are irrelevant to Roc)
it's interesting which things different implementations place value on
to me, approximately the entire benefit is being able to tell from the type annotations which functions are guaranteed to be pure and which are not
so not having effects in the type system at all strikes me as kinda "then what's the point?"
but I can appreciate that in OCaml's case that would be a gigantic breaking change to the entire ecosystem, so maybe that was never a realistic option :sweat_smile:
i think it's worth reading about why they added it/using it in a language like OCaml, and their perspective on the benefits, because it could change the set of things that are considered for this design/validate them/etc
Richard Feldman said:
yeah the variable name has to exist, at a minimum
I disagree. pure functions can always be promoted to impure. So the variable is not required
You can force that if a higher order impure function is passed in, all functions passed in must be treated as higher order impure functions
If you do that, no variable is needed to be exposed to the users
Given this is theoretically a very rare usecase I think you can get simplicity while still having most of the functionality.
(copy/pasting this from another thread)
if there's no effectfulness variable and I put this into the repl, what's the inferred type?
\fn1, fn2 ->
(fn1, fn2, (\arg -> fn1 "$(arg)!"), (\arg -> fn "$(arg)!")))
in all the current designs, the inferred type would be:
(Str -fx1-> Str),
(Str -fx2-> Str)
-> (
(Str -fx1-> Str),
(Str -fx2-> Str),
(Str -fx1-> Str),
(Str -fx2-> Str),
)
because the outer function doesn't call either of the functions it receives, it just passes them trough and creates new functions with them, none of which are effectful
Whatever our syntax for this is:
(Str -fx1-> Str),
(Str -fx1-> Str)
-> (
(Str -fx1-> Str),
(Str -fx1-> Str),
(Str -fx1-> Str),
(Str -fx1-> Str),
)
No need to print out the type variable cause they are all the same.
If either of the inputs are impure, all functions returned are treated as impure
Basically one impure function poisons the well.
That can be solved by splitting this into two functions, which probably should be done anyway since there seem to be two unrelated operations happening, but it's not ideal that we affect "unrelated operations"
But if this is what it takes for a cleaner syntax, it's a worthwhile cost to force people to split this kind of function into two
Brendan Hansknecht said:
Whatever our syntax for this is:
(Str -fx1-> Str), (Str -fx1-> Str) -> ( (Str -fx1-> Str), (Str -fx1-> Str), (Str -fx1-> Str), (Str -fx1-> Str), )No need to print out the type variable cause they are all the same.
oh so you mean in the specific case where they're all fx1 we don't need to say fx1 everywhere?
I'm saying that we ban having multiple effectfulness variables in a single function
Roc will always unify to exactly one effectfulness variable for a given set of inputs and outputs to a single function.
Which is always possible to do cause pure functions can be implicitly converted to impure functions
If you write:
\fn1, fn2 ->
(fn1, fn2, (\arg -> fn1 "$(arg)!"), (\arg -> fn "$(arg)!")))
if both fn1 and fn2 are pure, all outputs are pure.
If either of fn1 or fn2 are impure, all outputs are impure.
if both fn1 and f2 are impure, all outputs are impure.
Brendan Hansknecht said:
I'm saying that we ban having multiple effectfulness variables in a single function
but literally what happens if you put that expression into the repl
is the idea that if you take 2 polymorphic functions and pass them into a function which then returns them, even though they never interacted with each other except for having passed through this function, they are now coupled to one another just by having been passed to the same function and then returned by it?
yep
that definitely seems like incorrect behavior haha
It is a behavior to simplify the exposed surface of the type system. We have mentioned that we expect multiple effectfulness variables to be exceptionally rare. So I am suggesting we ban them to simplify the type system.
It is a concept users never have to learn.
I think we still probably want some syntax (maybe ~>?) to denote effect polymorphic functions, but then we don't need vars, yes
They just learn that higher order functions can be effect polymorphic, but if any higher order function is impure all must be treated as impure.
We don't need ~> if we do Ayaz's proposal
This also means aliases/opaque types don't need vars! They just write Mapper := (Str ~> Str)
I don't think for the initial version of this it's a reasonable idea to not have named variables at all
if it turns out they're so rarely used in practice that we want to consider taking them out later because we realize it'll be harmless and simplify the learning case, that's something we can discuss
but not having them at all, when in every other case type inference relies on them, feels like a "we're just gonna take a small part out of the foundation, what could go wrong?" and that feels like a proposition that's so fundamentally scary I don't think it could possibly justify the benefits for v1 of this :big_smile:
like if we want to have them but then choose to intentionally not use them at all in any of the builtins, and maybe not even teach them except in an obscure corner of the langref, that also seems fine
but I think not having them at all breaks assumptions about type inference that I think are extremely risky to break
(e.g. the cascading effects of making that repl example earlier infer to have all the type variables equal to each other could very easily break a bunch of examples that are totally unrelated and which we would expect to work, just because it would be such an unprecedented change to how type unification of those variables works)
Also, I know that you're against algebraic effects, but the current design sets us up to handle those, which is very exciting for me! Maybe someday...
This keeps our options open
well this is a form of algebraic effects, just without the "multiple types of effects" and without the "handlers" concepts
Exactly, yes!
Handlers is more what I was pointing to yes
to be pedantic, this is algebraic effects with both multiple effect types and handlers. the distinction is that handlers are only defined by the platform.
Brendan Hansknecht said:
Whatever our syntax for this is:
(Str -fx1-> Str), (Str -fx1-> Str) -> ( (Str -fx1-> Str), (Str -fx1-> Str), (Str -fx1-> Str), (Str -fx1-> Str), )No need to print out the type variable cause they are all the same.
If either of the inputs are impure, all functions returned are treated as impure
Basically one impure function poisons the well.
so what do you think about the design we talked about at some point where the syntax for "they are all the same" is just -> on its own?
If that is the case, and with what I have read so far (I'm sure I'm missing pieces), I would vote for this personally as a starter setup:
->: definitely pure
=>: definitely effectful
-var>: effect polymorphic
all effectful functions require ! to be called.
Effect polymorphic functions require ! unless the type variable can be resolved to always pure.
(as in, List.walk can be called without ! if it is passed in a pure function).
The ! can be removed later if we think it is simply noise or otherwise not worth having.
I really heavily want to push for starting with !. I think that given the number of new/beginner users in roc, if we start without !, many users will never realize that it exists. They won't see the benefits of it and it will just not use it cause they don't know the tradeoff. I think we really need to get a feeling for if ! is good or bad. I personally think it is super important for readability and keeping a strong separation between effectful and pure.
I think one of the downsides of requiring ! that we should discuss is that in practice, there will be a ton of !? in this design
specifically, everywhere you see ! in today's Roc will become !?
Not quite everywhere, but yeah, most. Cause we have a lot of error wrapping that would become task! ... |> Result.mapErr? WrapperErr
true
also like Env.dict! would not need it because it wouldn't return Result, but that's a rare case
I see mapErr more than bare !
really? :face_with_raised_eyebrow:
In user code, yes
oh you mean in this design it would be more common to see |> Result.mapErr? than bare !
Yes
I thought you meant that today mapErr! is more common than bare ! :laughing:
Lol, I'm lazy when typing on my phone
yeah I agree, I'd expect something! ... |> Result.mapErr? would be more common than bare !
My thought on !?:
something! ... |> Result.mapErr?! and ? separately.! sooner. So it will lead to the starter test running faster.I'm also definitely a proponent of ! being explicit if at all possible. I'm the guy at work that wraps function args in a struct so that the call site is readable without looking at the function signature.
Which ! gives us
I buy that, but I think the better way to run that experiment is to make ! optional (as in, just have it be accepted by the parser and then ignored) and then we can try the code both ways and see what we prefer
then we can either remove it or make it mandatory based on how that goes
That's a great idea!
Then we can duke talk it out (I enjoy comedic hyperbolics more than the next person)
well I'd say compare and contrast, but sure :laughing:
And then it's super easy to do what Agus did with the new import syntax, which is parse both and format to the desired one once we change
yeah exactly
makes incrementally transitioning (to either end state) easier
(as in, just have it be accepted by the parser and then ignored)
I don't think that will do justice to the experiment. A new user will probably never use !. They won't know the tradeoffs.
I mean if we format to auto add !, I guess I would be fine with the parser accepting either.
A random thought: if we're going to make it so calling an effectful function requires adding a bang, then another way we could mark functions as effectful is by insisting impure function names end with a bang. So:
fn : {} -> Result U64 Str # pure function
fn!: {} -> Result U64 Str # effectfull function
So the ! is not special syntax, it's part of the function name itself. Ruby is a sort-of precedent here, there's a cultural norm of letting 'dangerous' functions end with a bang.
Some advantages:
foo!? becomes easier to explain. The bang is there simply because it's part of the function name, so really nothing special going on here.We still have maybe effectful functions though. What do you write for List.map?
The "rule" there would be that if you define a higher level pure function, Roc will give you an effectful version with a bang for free. So we basically pretend there's two functions, one written by the programmer and one auto-generated.
I think it's very safe to assume that beginners will overwhelmingly react more positively to ? than to !? (I don't think we need to run an experiment to confidently say that!) - I think the relevant question is whether advanced users value the ! in the expressions enough to justify the downside of the increased learning curve
that's why I like the idea of making ! optional - it lets advanced users try it out without impacting the beginner experience at all
and then we can determine whether we (advanced users) like it enough to justify the learning curve cost etc.
Which we'd learn from them reading other people's code and complaining, I'd guess
yeah I mean I've done a lot of "showing syntax to people who only use mainstream languages and asking what they think of it" over the years and this is one of the few cases where I can imagine asking which one they prefer and the response being something like "...you're kidding, right?" :sweat_smile:
but that doesn't mean it's the wrong design overall!
just that it's super predictable which one beginners will prefer
and that has tradeoffs with other pros and cons etc.
Agreed. I'm saying we'd gauge the value of ! based on how much new users dislike it. I agree they're guaranteed to dislike it
"...you're kidding, right?"
No no no, trust me, it's better for you, by changing all code into emojis it enables all people regardless of literacy to understand
someone should record a video where they try to sell people on BF with a straight face as the future of programming
If you got someone like Jon Gjengset to do that, people would take it seriously
we (advanced users) like it enough
If we are benchmarking leaning towards advanced users, I am much more open to this. Cause I really do think this will be a care where new users (who probably don't even fully know if they want pure functions in general) would miss the benefits
yeah agreed
put another way - we already know the benefits of the current system; if we have the option of taking out the ! how much do we miss it?
(in practice I mean)
Richard Feldman said:
just that it's super predictable which one beginners will prefer
Well if you ask beginners why rust needs a "mut" whenever you mutate things, they will definitely tell you "are you kidding, just let the compiler figure out if this thing is mutated, why bother with the annotation"
I don't think those are on the same level :big_smile:
I wouldn't expect an "are you kidding" reaction from mut
but anyway, the broader point is that the advanced/experienced user preference is the thing I think we want to experiment with in this case
Having the formatter automatically add ! isn’t super easy, it would need to run the type checker :upside_down:
Coming next April 1st to a store near you: The official Roc keyboard with a U+203D key
Agus Zubiaga said:
Having the formatter automatically add
!isn’t super easy, it would need to run the type checker :upside_down:
Yeah, I thought of that after, let's just assume we'll reformat to remove if we remove, and give a type error in the other case.
Format to ‽
But how will I know if that is ?! or to !?
Haha
That doesn’t work I think, you need to pass an argument to the effectful function inside the result :smiley:
? ()! ?
Problem solved. I see exactly zero downside of interrobang
ikr
Jokes aside, if ! was required we wouldn’t need thunks would we?
we would need some kind of non-arrow syntax for annotations
Agus Zubiaga said:
we would need some kind of non-arrow syntax for annotations
I don't understand what you mean by this. I presume you're not referring to record builders?
No, just in general. The reason only functions can be effectful with purity inference is that you need to be able to decide when to run them.
If we require !, I think you can refer to them without performing them, like Task.
instead of Env.dict! {}, it could just be Env.dict!
If you want to pass the effect without running it, you omit the !
Not saying it’s a good idea, but maybe thunks are redundant in this world
I actually like the thunks personally
I think it makes it more intuitive to understand when things are vs aren't actually running yet
because intuitively, when you define a function it obviously doesn't run yet
and then when you call it, it intuitively does run
Yeah, makes sense
Last updated: Jun 16 2026 at 16:19 UTC