-
Notifications
You must be signed in to change notification settings - Fork 30
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
Naming things - unwrapping Eithers in boundary-break: .value or .? #118
Comments
|
|
Disclaimer: Bikeshed ahead! (My opinions about this are not that strong) I'm not a fan of neither While I don't have any good suggestions, though... Maybe something like |
@JD557 in general, yes, either is just a tagged union of equivalent types; but in terms of boundary-break, and the way it's used for error handling in ox we're taking the right-biased interpretation. |
I'd also add |
Uh, the worst part of Rust api :(
I'm still a big fan of the question mark operator. It's visually different,
suggests something non-typical is happening and symbolic operators are not
a very big problem if they are not overused and really signify some rather
well defined and very common operations (remember exclamation mark in akka
signifying async send?). Not a lot of people complain about sharks in CE
and ZIO after they got accustomed to them.
…On Fri 12. Apr 2024 at 10:31, Krzysztof Ciesielski ***@***.***> wrote:
I'd also add .unwrap for your consideration.
—
Reply to this email directly, view it on GitHub
<#118 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACBVNUSOMHZZG4S5JYQSOBLY46LWXAVCNFSM6AAAAABGDSN4VCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANJRGI4DINBWGY>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
@lbialy So it's the worst part, or is |
.unwrap()
…On Fri 12. Apr 2024 at 11:52, Adam Warski ***@***.***> wrote:
Uh, the worst part of Rust api :(
@lbialy <https://github.com/lbialy> So it's the worst part, or is ? ok? ;)
—
Reply to this email directly, view it on GitHub
<#118 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACBVNUUGGYIVBH32MBNF7XTY46VFTAVCNFSM6AAAAABGDSN4VCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANJRGQZTMNJVG4>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Personally, I've been using I haven't often run into cases where Alexandru's critique would apply because def loadTemplate(): Source Or Err = Or.Ret:
"template.qmd".path.slurp.?.tap(lines => parse(lines).?) (The The point of using If you don't have a favored branch and a disfavored branch but rather two co-equal branches, you should not have a single accessor but something like extension [L, R](either: Either[L, R]) {
/** Exit to boundary matching the left branch, or keep going with the right branch's value. */
inline def rightOrBreak(using Label[L]): R = either match
case Right(r) => r
case Left(l) => boundary.break(l)
/** Exit to boundary matching the right branch, or keep going with the left branch's value. */
inline def leftOrBreak(using Label[R]): L = either match
case Left(l) => l
case Right(r) => boundary.break(r)
} However, even though I wrote them, I never use these, or variants that have I'm not voting here because I've already made my own choice with my own library, but I wanted to share some experiences. |
This is also something Martin seems to be keen to introduce to the language, given his keynote from 2023. I don't really understand @alexandru's comment about |
A weekend though - since we are considering symbolic operators, maybe we're incorrectly fixated on So ... maybe e.g. |
The fewer braces syntax is here to stay, and, unless it gets deprecated somehow, will probably become the dominant style in Scala 3, once backwards compatibility with Scala 2.x is no longer a concern. This syntax isn't experimental, and there's no in-between. Another outcome would be for Scala to have multiple syntaxes, forever, but that's not very Pythonic, and it's not a very likely outcome, due to conformity nowadays being forced by Scalafmt. Although, a Klingon language option would be fun. This is valid syntax, showing method chaining in action, and it's less than ideal for the obvious reason that validate(user).flatMap: user =>
fetchRepository(user)
.?
// ...also this...
validate(user)
.flatMap: user =>
fetchRepository(user)
.? I've always disliked special operators. I think all special operators in Scala-land suck, making the language actually harder to read. I'm not fond of operators such as
The argument that Martin may want to make Something to think about 🙂 |
|
Would be nice to be able to define `.ok?` or even `.left?`/`.right?` but
parser doesn't seem to like this kind of stuff.
…On Mon 15. Apr 2024 at 13:27, Jonathan Winandy ***@***.***> wrote:
.get ? Like for options, we know already it means something special!
—
Reply to this email directly, view it on GitHub
<#118 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACBVNUS3GQ3C6FSJISD3YT3Y5O2SJAVCNFSM6AAAAABGDSN4VCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANJWGU4TQOJWHA>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
I don't recommend
val m = Map("a" -> 1, "b" -> 2)
val number = attempt:
safe{ "w".toInt }.!
.attempt:
m.get("c").!
.default:
0 It's...okay. I find
|
Honestly, that's super-duper obvious to me. How do you not see it? I mean, sure, before you're used to it there is the whole, "Hey, kitty, what are you doing??" thing. But it's not invisible--it's the only thing on the line! It's the in-line usages that might be easier to overlook: validated(readTable(t).?, readOptions(o).?).? ++ getDefaults().? |
Having a non-symbolic alternative is good practice anyway. In kse3, I used extension [X, Y](or: X Or Y)
/** Exit to boundary matching the disfavored branch, or keep going with the favored branch's value. Like `.?` but unwraps Alt */
inline def getOrBreak(using Label[Y]): X = or.fold{ x => x }{ y => boundary.break(y) } which is almost the same save Either way, I think |
Parser would not like `result.ok?` or `either.right?` because for some
reason you can't define an identifier ending with a symbol.
…On Mon 15. Apr 2024 at 19:48, Ichoran ***@***.***> wrote:
So better make sure that .? will actually become official, and also
provide an alternative for those of us that will hate it anyway
Having a non-symbolic alternative is good practice anyway. In kse3, I used
extension [X, Y](or: X Or Y) /** Exit to boundary matching the disfavored branch, or keep going with the favored branch's value. Like `.?` but unwraps Alt */
inline def getOrBreak(using Label[Y]): X = or.fold{ x => x }{ y => boundary.break(y) }
which is almost the same save .? does not unwrap the Alt and getOrBreak
does, but maybe that's a mistake on my part and the canonical way should
just be to use .? as a symbolic alternative to .getOrBreak.
Either way, I think getOrBreak is the natural linguistic parallel to
getOrElse, which is the other way to handle the disfavored case.
—
Reply to this email directly, view it on GitHub
<#118 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACBVNUVPSATFXTYONY3LFI3Y5QHHJAVCNFSM6AAAAABGDSN4VCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANJXGQ4DKNRSGE>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Oh, that's right, I misunderstood before. It would have to be Another idea is using |
Hahah, yeah, `.please` does sound nice and it's visually remarkable too, I
have never seen "please" used in code as a method name or field name. Maybe
this is the way to go, polite flow control operators. With `.?` alias
signifying a polite "hmm?". I bet we could even implement a scalafix rule
verifying that your code is polite enough for next April's Fools.
…On Mon 15. Apr 2024 at 22:46, Ichoran ***@***.***> wrote:
Oh, that's right, I misunderstood before. It would have to be .ok_?,
which is clunky to type.
Another idea is using .please. In addition to your code looking very
polite, "please" suggests that there is some doubt about whether it's going
to happen, but you asked nicely, so if it isn't going to happen the
alternative should be nice too (which jumping out with the disfavored value
is).
—
Reply to this email directly, view it on GitHub
<#118 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACBVNUVSRJBDOD5GIXRJINLY5Q4C3AVCNFSM6AAAAABGDSN4VCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANJXG43TOOJSG4>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Maybe validated(readTable(t).ok, readOptions(o).ok).ok ++ getDefaults().ok |
With a grain of salt :
val a = expr.let
(Context allows computation or not)
Maybe a new keyword?
|
@odersky - I like the question mark better, but |
Since we're talking about the direct style here, I feel that How about something more persuasive, e.g. |
@ghik - The problem is that |
@Ichoran unironically I have defined I do think that something like Of course it's imo just bikeshedding, once we use it for a while I believe it'll become more familiar no matter what we choose. On the other hand I would not go with |
Thank you for all the input :) In the end I'll go with Note that I think I'll use |
|
Are these operators going to be implemented with an empty parameter list or
without one at all? I think we had a convention that side effecting methods
and functions should be called with an empty param list. I dislike this
convention immensely personally and control flow change is a bit different
from "having side effects" but if there's a consensus that it's a good
thing for readability maybe we should retain it?
…On Sat 20. Apr 2024 at 17:13, Ichoran ***@***.***> wrote:
ok() opens a possibility of ok(inline f: X => L) as nicely syntactically
compatible, because once you use this you will find that left-mapping prior
to .ok() is rather a drag (and slows things down), but your types don't
always match what you need.
—
Reply to this email directly, view it on GitHub
<#118 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACBVNUULXKVUFAUH63LES2LY6KA2ZAVCNFSM6AAAAABGDSN4VCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANRXG4YDGMZTGM>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
It's not clear to me what's the convention for the empty parameters list, e.g.,
The I think that beyond any useful conventions we can have, the technical difference between |
Just a clarification:
Have we agreed on precise definition of "side-effecting"? If you said "not referentially transparent" instead, it would be unambiguous. (I really hope to not start a lengthy, academic-ish discussion about purity, RT and side effects here 😅) |
@ghik I did some research on that, and I'm using the following definition:
@alexandru as for the |
There was a discussion about this on Scala User just recently. There's Seth Tissue gave a link to an earlier discussion: https://users.scala-lang.org/t/paramterless-functions-with-and-without-parentheses-different/9939/12?u=odersky |
@odersky Thanks! There's a couple of approaches proposed there, to include
I guess it's moot to discuss |
It would be nice to have a more precise definition of a test for "no side effects", similar to how referential transparency is often used as a test for "purity". For example:
For example: val it = Iterator(1,2,3)
it.hasNext // removing this invocation does not change observable behavior
println(it.next()) This of course includes all pure (RT) functions, but also things like This is pretty much the same as @adamw definition, but it provides a more tangible method to prove that a function is effect-free. |
@ghik in your example, doing From my POV, it's trait IEnumerator[+A]:
def moveNext(): Boolean
val current: A In this case, I'm just saying that the current convention isn't well established, and the only meaningful difference is that without parens, you can override it with a Having parens in |
It's a murky area, since the only well-defined criterion is referential transparency, but we (collectively) hesitate to enforce it strictly. Specifically for I realize these are not very strong arguments. |
I just wanted to leave an encouragement to try out The patterns you get aren't always the most efficient, but boy are they easy to write! You hardly even need to make futures monadic at that point. If |
@Ichoran the question is, is |
I'm not sure if it's useful for more than familiarity. I have yet to figure out a substantial value-add to provide with it. Reading left-to-right is an advantage, but you can always |
I guess if you want it exactly the same as |
@Ichoran I think this example would rather be: |
So Anyway, what I was advocating for was that In my library I can do things like: val lastnames = Fu:
val lines = p.slurp.?
lines.map(q => Fu(db.ask(q).?.lastname)).map(_.?) and it all just works: in a thread I slurp a file, terminate early with any error, then take each line as a lookup, etc. There isn't any distinction between normal error handling and future error handling, not in the least because, in ox notation, either:
fork:
unsafeOperation.ok is a bug. |
Sorry, So in what you propose, In your Maybe the difference comes from the fact that in Ox we don't try to interfere in any way with exceptions - if they're thrown, we just do the appropriate cleanup to make sure there are no leaks. But they are mostly considered "panics". If you want typed errors, you should then use |
Well, I'm not sure this is exactly the right place to discuss it, but I'll try to explain my reasoning. (It's also present to an extent in the kse docs here and here.) I'm going to re-explain things that I think you already know so that I don't wrongly assume some part which ends up leaving examples confusing. The starting premise is that we have a good means of handling errors in direct style. I wasn't satisfied with Furthermore, I favor direct-style jumps to handle error conditions, for which I use def loadAdmin(path: Path): User Or Err =
Err.Or: // This is the boundary
val lines = path.slurp.? // slurp might fail; jump to boundary
val user = User.parse(lines).? // parse also might fail
user.administrator // Let's suppose that this can't fail because fallback is the user administers themselves Now, given that, what are the key type signatures of a future? We need to be able to start a computation that in general might return something, so that's The first unification of concepts is that with virtual threads, you don't worry about blocking. There's no reason to keep typing The second unification of concepts is that And the third unification of concepts is that if something fails within a thread, you probably want to just terminate with that error, so rather than writing // Use of future is pointless save for being illustrative
def example(): Bar =
Err.Or:
val future = Fu:
Err.Or:
unsafeOp.? + safeOp
val value = future.?.? // Once to unwrap Future, once to unwrap error inside
value.bar
.getOrElse(Bar.default) you should just re-use the future's own I-might-fail-because-I'm-a-concurrent-operation capability with the contents' I-might-fail-because-reasons. If you want to distinguish them, you can pack that information into the error type. But if you're re-using the error capability anyway, every thread boundary is an error-handling boundary. So why not empower the user to use it that way? Thus: def example(): Bar =
Err.Or:
val future = Fu:
unsafeOp.? + safeOp
val value = future.?
value.bar
.getOrElse(Bar.default) This makes everything both highly convenient and highly safe. Since you seem to be taking a rather similar attitude towards at least some of how ox works, I wanted to suggest that you consider a similar pattern. The open question I have is whether it is still too easy to accidentally have things cut across thread boundaries. |
After several hours of work with Ox (on streams where I build scala.today) I have to say that I concur with many of the remarks above. Either handling being somewhat separate and parallel to ox's concurrency stuff leads to painful pitfalls. A big chunk of said issues come from the fact that I also have a gripe with nested either blocks as they can easily hide issues: |
@lbialy - My kse3 library handles those issues by discarding So, yes, all of what you said. kse3 fixes it; ox can too, but probably has to make similar tradeoffs. Even then, though, there's always a danger of val outer: Int or Boolean = Or.Ret:
val inner = Fu:
val thing: String Or Boolean = Alt(true)
thing.?.toInt
.ask().getOrElse(_ => false) The thread running This isn't extremely hard to avoid if you keep methods small and logic clean, but if you were using a monadic style instead of a direct style, it would be a pile of type errors that need to be fixed with monad transformers and such instead of a runtime error. |
Or you can just prevent capture of .? on forks. With #146 this:
no longer compiles. |
@Ichoran great explanation, thank you! That should almost be a blog, would be a great entry to ScalaTimes :) One thing I don't understand:
Where is the boundary interception happening? Is But otherwise, I'm almost convinced that |
@adamw - The The boundary interaction happens at Which, incidentally, is a bug. The return type would be typed as I should always paste my code into the REPL 😆 |
@adamw I agree that fork could open an either scope if a tighter
integration with Either datatype is the direction you want to take. I'm
fine with that, whatever reduces the numbers of gotchas and extra steps
while makings things more uniform and less surprising is fine in my book.
There's still a question of preventing dumb mistakes though.
…On Thu 6. Jun 2024 at 19:24, Ichoran ***@***.***> wrote:
@adamw <https://github.com/adamw> - The Alt(true) is just creating the
disfavored value; Alt is isomorphic to Left; Is corresponds to Right.
The boundary interaction happens at thing.?. It's supposed to return a
String, but it can't, so it jumps with the disfavored value, the Alt(true).
This tries to take it across the Fu: thread boundary, which would take an
Alt[Err] but not an Alt[Boolean], up to the Or.Ret: boundary. But, of
course, the Fu thread is running on its own, so there's nowhere to jump
to; the result ends up as an exception. ask() gives that exception as an
Alt[Err], and then getOrElse has to take the or-else branch, which sets
the value to false.
Which, incidentally, is a bug. The return type would be typed as Int |
Boolean as I wrote it. It should have been mapAlt(_ => false).?, or
Or.FlatRet: with no .? jump.
I should always paste my code into the REPL 😆
—
Reply to this email directly, view it on GitHub
<#118 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACBVNURHW4AKR32FFWOA5ILZGCLL3AVCNFSM6AAAAABGDSN4VCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCNJTGA2DIMBTGM>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
A small change, but one thing that might help when working with typed errors & forks is the ability to join & unwrap in a single call, so given a Providing any special handling for exceptions with forks doesn't make that much sense, as any exception thrown in a fork will be end the scope (if it's supervised - which is the default). So in a supervised scope, For more utilities & feedback (thanks @lbialy & @Ichoran, very valuable!) let's use the other PRs & new issues. (btw. - if you would be willing to publish some of the scaladocs from kse3 as a blog post, I think many people would be interested and benefit from it) |
@adamw - I'm happy to write a blog post for ScalaTimes or somesuch. I'll email you to discuss details. |
Recent ox releases include a boundary-break implementation specialised to
Either
s, which allows you to write "unwrap" anEither
within a specified boundary, for example:Here,
.value
is used for the "unwrapping". However, one alternative would be to use the Rust-inspired.?
. On the one hand, @lbialy argues that.value
is clash-prone, along with @Ichoran who says.value
suggests that it's a safe accessor.On the other, symbolic operators historically didn't work out that well, plus, as @alexandru points out, it looks awkward and doesn't play well with fewer-braces.
What's your take? Vote with a 👍 on a comment with your pick, or propose your own!
The text was updated successfully, but these errors were encountered: