Follow

Can other languages solve the semipredicate problem (en.wikipedia.org/wiki/Semipred) in a way that's like to this solution?

Imagine you want a `safe-div` subroutine that doesn't throw an error when the user divides by zero. Here's one approach:

sub safe-div(Numeric $a, Numeric $b --> Numeric) {
if $b != 0 { $a / $b }
else { 0 but role { method divided-by-zero { True }}}}

Then you can call .?divided-by-zero on any 0 returned, but the 0 is still a 0/can be used without destructuring.

@bugaevc

For all the usual reasons you'd want to solve the semipredicate problem. E.g., you want the benefits of a type system, but want to signal an error without needing to use a wider return type.

Other languages solve the same issue with multiple returns (go) or algebraic types (rust), but both of those require a bit of work from the caller on the happy path (and can sometimes lose info – in rust, if a fn calls f.unwrap_or(0) and returns 0, the caller can't know if it's using a default

@codesections
let result = divide(42, 0);
use(result.unwrap_or(0));
if result.is_err() {
handle();
}

No?

@codesections or perhaps you want something like this:

enum ValueOrDefault<T, E> {
Value(T),
Default(T, E),
}

impl<T, E> ValueOrDefault<T, E> {
fn as_value(&self) -> &T {
match self { ... }
}
}

...but I still don't see the point. Do you have an example of a use case when you'd want this?

@bugaevc

Yeah, that's what I meant about the calling code needing to do some work. `divide` returns a Result, and the caller needed to unwrap it. But you can't *both* return a plain i32 (or whatever) that the caller can use without unwrapping *and* return info about how it was generated. E.g., if I get 42 from i32, I can't know whether it wrapped or not (without looking at my calling code, I mean)

But yeah, it's mostly about ergonomics on the happy path. And Rust's ? gets very close

@codesections it was an intentional design decision in Rust to make you handle the possibility of error in some way, before you continue with the happy path.

But your original question was not about Rust, so I'm trying to understand the advantages of that approach. It returns a sentinel value that you can immediately use, and also info on whether there was an error, but you're not expected to check for there having been an error? Why?..

@codesections I guess perhaps this would make sense for an operation that has a very sensible default; you'd then use this default as the sentinel value, instead of making the caller substitute the default value.

@bugaevc

Agreed that Rust's design choice was intentional (and is a good fit for it's target use cases).

Here's one toy example: imagine some object that maintains a stateful counter that wraps counter with something like .wrapping_add; and 90% of the time, users don't care whether the counter has wrapped, but sometimes do. With most languages, you'd probably have one method that returns the value and another .has_wrapped method, but "annotating" the returned value provides an alternative.

@codesections interesting; in this case I wouldn't say that the case where it has wrapped around is *an error* — the answer is just as valid (*not* a sentinel value!), but we also additionally know whether it has wrapped around or not. So something like -> (u32, bool) sounds fitting.

@codesections

In Smalltalk, nil is a full-fledged object. The entire heirarchy implements the method 'ifNotNil:' which takes a block (i.e. lambda) argument and evaluates it. 'nil' overrides this to do nothing. There's also 'ifNil:' which does the reverse.

So methods can return 'nil' as an out-of-band error message and handle it gracefully:

status = rocket enableGuidance
ifNotNil: [ rocket enableBattery ]
ifNotNil: [ rocket launch ].

(ctd)

@codesections

There's also the 'Option' type from the Functional world:

en.wikipedia.org/wiki/Option_t

Basically, you create a box that holds either your result or nothing. A function applies a lambda to the value of the box (or not if it's empty) and returns the result of the lambda in a new box, which may be empty if there was an error in the lambda.

In this way, you can chain your computations together with no error check until the end. Any error stops the dependent operations.

(ctd)

@codesections

Scala has this pretty firmly embedded in the language; every type has its own null subtype created automatically.

But lots of languages do this. Even C++17 has an Option type.

@suetanvil

Yeah, I write a lot of , which also makes heavy use of Option (and Result). :)

The difference between what I'm talking about and Option is that the pattern I'm showing lets you return a value that's annotated with extra info but that can be used as the value type *without* unwrapping it (e.g., it still pattern-matches as an Int, not an Option<Int>)

Of course, sometimes you *want* to force the caller to handle the None explicitly, but sometimes you don't

@suetanvil @codesections

> In Smalltalk, nil is a full-fledged object. The entire heirarchy implements the method 'ifNotNil:' which takes a block (i.e. lambda) argument and evaluates it. 'nil' overrides this to do nothing. There's also 'ifNil:' which does the reverse.

Is Nil falsely? If so, is there any difference between Nil.ifNil and `if (!Nil)` or whatever the analogous syntax would be?

@codesections

Smalltalk doesn't really have control structures; it's all done by passing lambdas around. So the booleans each have their own type and each implement their own 'ifTrue:' and 'ifFalse:' to do the expected thing.

So for

a > b ifTrue: ['bigger!' print].

the '>' expression returns either true (whose 'ifTrue:' evaluates its argument) or 'false' (whose 'ifTrue:' does nothing.)

So the 'ifNil:' thing is as much of a control structure as basic booleans.

(ctd)

@codesections

But in answer to your question, nil is not falsy. There's no truthiness in Smalltalk. However, there are 'isNil' and 'isNotNil' methods in the heirarchy so you can do stuff like:

foo isNotNil ifTrue: [ '!nil' print ].

or

(foo isNotNil and: [bar isNotNil ])
ifTrue: ['!nil' print].

@codesections

The cool thing about this stuff is that it's really easy to roll your own control structures.

In one of my RogueLike game attempts, I wrote a boolean-like success/failure class heirarchy so that I could encode other information about why something didn't work. The base classes implemented 'ifSuccess:' and 'ifFailure:' in the expected way but I could also return a SpellFailed or a AttackRepelled depending on why it failed and code that didn't need to care could ignore it.

@suetanvil

Yeah, that's another good idea Raku steals: a boolean context (e.g. using `if` or the ? operator) calls an object's .Bool method, so it's easy to create a SpellFailed that works well with existing control structures — and you don't even need to inherent from anything

@codesections

Larry Wall has always been pretty good at breaking into other languages’ houses and stealing the good bits.

(I’m on the fence about learning Raku. Maybe when I’m bored with Ruby.)

@suetanvil

> (I’m on the fence about learning Raku. Maybe when I’m bored with Ruby.)

Well, for whatever it's worth, I've become somewhat obsessed with over the past year. I now write it pretty much every chance I get (I still plan to dip into Rust for truly performance-critical code, but the two seem to play well together)

@suetanvil

> Smalltalk doesn't really have control structures; it's all done by passing lambdas around. So the booleans each have their own type and each implement their own 'ifTrue:' and 'ifFalse:' to do the expected thing.

Interesting. I really ought to budget an afternoon to playing around with Smalltalk. Even though I usually lean hard away from OOP, it has some interesting ideas.

@suetanvil

And I know Alan Kay has said

> I made up the term 'object-oriented', and I can tell you I didn't have C++ in mind

I really ought to get around to seeing what he *did* have in mind. (Also on the short-list for related-but-different reasons: Erlang)

@codesections

AIUI, OOP as we see it was an attempt at achieving Alan Kay's goal. It failed but turned out to be useful on its own.

But FWIW, I didn't really grok OOP until I learned Smalltalk.

@codesections

Alan Kay’s vision for OOP requires resilient objects that can recover from errors in other objects as a default thing and doesn’t really work in existing languages.

(Although I’ve heard that Erlang processes kind of work like that. I need to learn Erlang at some point.)

@suetanvil @codesections learnyousomeerlang.com/ is free and excellent. And yea erlang processes and supervisors are very OOP from certain perspectives

@dch @codesections

I have a copy and am slowly working through it.

(Advice I got was (tl;dr) learn to read Erlang, then jump directly to Elixir.)

@suetanvil @codesections I would only recommend Elixir to people who are new to functional programming, or to the erlang ecosystem. The big advantages of elixir are significant:

 better docs. way better. searchable. usable. guides.
 functional dev tooling. Erlang's dev tooling sucks. Always has, always will.
 consistent module and function naming. A lot of Erlang's modules have parameters in an inconvenient order
 string handling in Elixir is 1000x (I measured this carefully) nicer.
1/2

@suetanvil @codesections That said, there are 4 really good books:

 Elixir -> I would start with this one manning.com/books/elixir-in-ac IMO the best book across the whole erlang + elixir ecosystem
 Erlang -> learnyousomeerlang.com/ as mentioned
oreilly.com/library/view/desig when you understand the first one and want more
 this also erlang-in-anger.com/ free ebook, from an operational perspective also from Fred Hebert essential reading ferd.ca/ 2/2 sorry for the thread hijack

@dch @codesections

It’s fine. I can always use more resources.

(I take it you mean’t to say Erlang instead of Elixir two toots back?)

@suetanvil @codesections my wording is ambiguous. I mean that people should jump directly to elixir. I am personally fond of erlang but it is a strict subset of elixirs capabilities and elixir has both a more active community and better on ramps wrt docs and books. You don’t need to learn erlang to do elixir they are fully interoperable with no performance compromise

@dch

Do you have an opinion on "Introducing Elixir" by Simon St.Laurent & J.David Eisenberg from O'Reilly?

I ask because I got a copy in a bundle and am wondering if it's worth my time.

@suetanvil I've not read either of these unfortunately sorry. Let me know how you get on!

@suetanvil @codesections I would only recommend Elixir to people who are new to functional programming, or to the erlang ecosystem. The big advantages of elixir are significant:

 better docs. way better. searchable. usable. guides.
 functional dev tooling. Erlang's dev tooling sucks. Always has, always will.
 consistent module and function naming. A lot of Erlang's modules have parameters in an inconvenient order
 string handling in Elixir is 1000x (I measured this carefully) nicer.
1/2

Sign in to participate in the conversation
Fosstodon

Fosstodon is an English speaking Mastodon instance that is open to anyone who is interested in technology; particularly free & open source software.