curious what anyone thinks about this idea!
https://docs.google.com/document/d/1a51n7eIEbPjCWnGaL-pWbZBsRfi55GVQwIdPQTUu49Q/edit
Radical idea: Remove Bool and use Result {} {} like what was done with Maybe
Opaque bools sounds like a good idea!
Edit: Another advantage is that it avoids accidentally ending up with a type like [ True, False, SomethingElse ]* which is almost certainly a mistake.
I was skeptical at the start, but after reading the whole thing, you have convinced me!
The 'pit of success' argument is a very good one, in my opinion :+1: .
Yeah I definitely think it would be nicer to have Bool as a built-in!
I also agree they shouldn't be overused, for sure... but I still want to be able to use them in quick-and-dirty first-draft code, and it feels a bit "heavyweight" to have to actually define Bool.
So this seems like a good balance.
If kand ki were already in scope, I'd use true = \ x, y -> x and false = \ x, y -> y (those functions are the Church Encoding for the Bools values). But anyway, I like the Opaque Bool idea.
Church encoding booleans: https://en.wikipedia.org/wiki/Church_encoding#Church_Booleans
@Hashi364 Then you should do it for lists as well ( https://aphyr.com/posts/340-reversing-the-technical-interview )
:stuck_out_tongue_wink:
Cool! These days I've been playing with "list destructor" (not sure if it's a official name) in Haskell-land:
data List a = Nil | Cons a (List a)
list :: b -> (a -> List a -> b) -> List a -> b
list b _ Nil = b
list _ f (Con a as) = f a as
:P
This is a fun an exciting plan. :)
I don't like the current raw definition of booleans as tags, but I love tags. I didn't understand the root cause of my hypocrisy until reading this doc - the answer is that they are special, but other tags are often preferable :)
I rarely match on bool in OCaml but I frequently match on record/tuple product types of which a bool is a term. So anything that favors the ergonomics of that pattern is preferable even if it feels odd to fully qualify something so simple.
Something to consider though: frequently people judge complexity based on "how hard is this thing I consider simple." Think "hello world" or "fizzbuzz". It's why I have a hard time demonstrating OCaml/dune and Haskell. s-expressions and IO aren't really that bad (but way worse than Bool.true), but it hasn't mattered to colleagues -- they see something completely new on the way to something they think trivial and bail as unnecessary complexity (an instinct a lot of very good product engineers cultivate). It's why I love how types in Roc work! You don't need to see them until they become necessary complexity.
My 2c anyways from pushing FP from within product teams. I guess it is a vote for what I think OCaml does here with a Pervasives module with a few global types/functions.
It adds an entire special language keyword whose only real benefits are to make boolean values more concise (which is arguably a downside) and to make it look more familiar to beginners. I value Roc having a lower learning curve, but I think the learning curve of "you write true as Bool.true in this language" is extremely small; more than anything, this would be about familiarity.
This seems like a circular argument given that the context for this point is "Suppose we try this and it isn't nice in practice. What then?" but then the reasons for why you wouldn't choose this option are the same reasons you originally gave for why you want to try opaque bools. It seems like if opaque bools aren't nice in practice it would be precisely because it turns out _not_ to be a downside for boolean values to be more concise and because the learning curve is higher than anticipated or causes more confusion than anticipated.
I think this idea is worth trying but if it fails I would probably fall back to true/false as keywords rather than making them tags, since I think the tags approach is likely to cause subtle confusion. At least with Bool.true any confusion is likely to be really explicit: Why is it Bool.true and not true? On the flip side if you go around using True and False you might not realize you're just using plain old tags and this could lead to you making mistakes.
yeah totally agree - if Bool.true doesn't work out in practice, my next choice would be true
I thought of an interesting Ruby analogy: right now it's as if we use the symbols :true and :false for our booleans.
It works, but it means we can't really tell when the value we have is semantically a boolean vs something that happens to share a name with one.
Found this issue related to this thread. Is someone already working on this? If not, I would like to start working on it. Is it just a case of changing the type alias to an opaque type and changing all instances of it in tests and source code?
Or do I also have to change the inner workings of some functions like Bool.and and Bool.not?
I would probably also have to change the intrinsics of the if then else expression? Maybe this is above my capabilities :sweat_smile:
No one is working on it yet, it would be great if you wanted to work on it! It should only require changing https://github.com/roc-lang/roc/blob/main/crates/compiler/builtins/roc/Bool.roc and usages of booleans, I believe. I don’t think you should have to change anything deeper than that, though I or anyone else would be happy to help you through that if it turns out to be the case.
alright I will try and implement it then
I got something working (still have to update examples and documentation though).
However when I use the new Bool.true or Bool.false in the repl, they get printed as True : Bool and False : Bool. I think they should be printed in lowercase however. How would I change that?
ah good question
In some sense it's not totally wrong, because I'm assuming you have defined Bool := [False, True]?
but, what you will want to do is change https://github.com/roc-lang/roc/blob/6613e9d3472b3cb24c92134b97655743026f6181/crates/repl_eval/src/eval.rs#L1147-L1151 to be something like
Alias(alias_name, ...) => {
if alias_name() == Symbol::BOOL_BOOL {
return if value {
Expr::Var { module_name: "Bool", ident: "true" }
} else { /* resp. for false */ }
}
...
}
No it's not wrong, but if the type is opaque, what is printed should not show it's implementation in my opinion. Especially for beginners this could be confusing
agreed
Oh and I just tried out an if statement with a normal True tag. It compiles as if it is a normal bool. So I think I will have to dive a bit deeper into the compiler
good catch. then you will need to update https://github.com/roc-lang/roc/blob/6613e9d3472b3cb24c92134b97655743026f6181/crates/compiler/types/src/subs.rs#L1732-L1751 as well
how would I need to update that? I feel like if I just delete it all, the opaque type won't work either
It should be enough to change https://github.com/roc-lang/roc/blob/6613e9d3472b3cb24c92134b97655743026f6181/crates/compiler/types/src/subs.rs#L1749 to AliasKind::Opaque
don't I have to specify in from which file or package or whatever the opaque type is coming?
Say someone else (for some inexplicable reason) decides to make another opaque type with True and False tags, how would these be distinguished? Or should we assume no one will do that since it is in the roc source itself?
The name Symbol::BOOL_BOOL takes care of that. Opaque types are compared nominally. So only opaque names with the same name are equivalent.
Oh I see, thanks!
When I run the tests I get a bunch of panics in rust like
thread 'gen_tags::applied_tag_function_pair' panicked at 'invalid cond_layout: Erroneous', crates/compiler/mono/src/ir.rs:6265:18
any idea what the problem could be?
probably a clash with the old and new Bool definition
Have you run the type checker tests first? They can be run with cargo test -p roc_solve
running it now
I have changed all llvm-gen-tests containing True or False to their corresponding true and false though
I have some errors when running the type checker tests
Will try to fix them
There are a lot of explicit [True, False] type aliases in these tests
I assume I shouldn't change them as they test tag type inference?
yeah the explicit ones shouldn't be fixed. unless they are being used in if conditionals or == or !=
It's possible you are seeing the mono errors because there are things like if True or True == in crates/compiler/test_mono/src/tests.rs
Ayaz Hafiz said:
yeah the explicit ones shouldn't be fixed. unless they are being used in
ifconditionals or==or!=
Sometimes there are functions which are explicitly typed with Bool, yet pattern match on True and False. Should I change these to if expressions, or change the type?
Ayaz Hafiz said:
It's possible you are seeing the mono errors because there are things like
if TrueorTrue ==incrates/compiler/test_mono/src/tests.rs
Will look at it later
Kilian Vounckx said:
Ayaz Hafiz said:
yeah the explicit ones shouldn't be fixed. unless they are being used in
ifconditionals or==or!=Sometimes there are functions which are explicitly typed with Bool, yet pattern match on True and False. Should I change these to if expressions, or change the type?
solve_expr::inference_var_inside_tag_ctor is such a test
Should I change these to if expressions, or change the type?
It's probably best to change them to a type like MyBool : [ True, False ] so that they can continue to pattern match
Yeah, for that one
badComics: [True, False] -> [CowTools _, Thagomizer _]
badComics = \c ->
when c is
True -> CowTools "The Far Side"
False -> Thagomizer "The Far Side"
badComics
or the MyBool alias would be fine
Okay, now there are only two errors left.
one is with string_starts_with, for which I have to change its source I suppose. I'll do this later.
the other is with mutually_recursive_types, for which I don't get the type syntax
What test is the mutually_recursive_types one? I can't see a test of that name
oh my bad, it is mutually_recursive_captures
I still don't get it, but I fixed it by changing the [True]* and [False]* both to Bool in the type signature
and x and y have been changed to Bool.true and Bool.false
oh yeah that works. thanks!
now in the test_mono lots of tests failed :exhausted:
I don't understand these at all
Do you have a branch I can pull down? Happy to hop and take a look
I will commit and push quickly
the mono tests probably just need to be regenerated
How would I do that?
Also what do they do exactly? At this point I feel like I am just doing magic with Ayaz whispering the spells in my ear :joy:
Ayaz Hafiz said:
Do you have a branch I can pull down? Happy to hop and take a look
https://github.com/KilianVounckx/roc/tree/opaque-bool
okay cool, it looks like you did update the mono tests but you're seeing a lot of these Erroneous errors?
exactly
okay yeah there's more going on here than I anticipated :sweat_smile: chasing it down now
I will go ahead and look at the examples too see if they need updating
once you find the issue, let me know, or update it yourself. Whichever suits you
ah okay
so, the builtin files need to be updated
the ones in crates/compiler/builtins/roc/*.roc
However, if you just run roc check (or cargo run -- check ) over them, you won't see any errors.
That's because we cache types, which silences type errors. So you can see the errors, and update all of them by doing
ROC_SKIP_SUBS_CACHE=1 cargo run -- check crates/compiler/builtins/roc/List.roc
and respectively for each builtin file
after that, the mono tests should start working again! hopefully, anyway
Will try that
I just updated most of the roc files with a good old search and replace, manually checking if they should be replaced
The False interpreter got me good :sweat_smile:
quick question (I think it is not related, but not sure)
in the crates/compiler/load_internal/tests/fixtures/build/app_with_deps/ManualAttr.roc file, there is a when True is statement.
Should I change anything about that?
Ayaz Hafiz said:
That's because we cache types, which silences type errors. So you can see the errors, and update all of them by doing
ROC_SKIP_SUBS_CACHE=1 cargo run -- check crates/compiler/builtins/roc/List.rocand respectively for each builtin file
I ran all of those. No errors there
in the crates/compiler/load_internal/tests/fixtures/build/app_with_deps/ManualAttr.roc file, there is a when True is statement.
Should I change anything about that?
Nope, that's fine as is
I ran all of those. No errors there
Do you mean there were errors and you fixed them, or you didn't see errors to begin with?
No errors when type checking roc after fix
the mono still fails on some tests though
thread 'quicksort_swap' panicked at 'Output changed: resolve conflicts and `git add` the file.', crates/compiler/test_mono/src/tests.rs:200:9
that should be okay, for those you can just commit the changed file
Yeah was just gonna say, git says some files were changed in test_mono/generated
I assume this is normal then?
Would you mind explaining what is generated? I like to understand what I'm doing so I could replicate it in the future if needed
awesome, the mono tests are passing now as well!
Sure thing. Those tests generate an intermediate representation of a Roc program that we use in the different backends we have (LLVM, wasm, dev backend) in order to generate an executable. We call that intermediate representation "mono", because it monomorphizes a program, amongst other things. The tests serve as a snapshot, so we can check when things go wrong in the conversion from Roc source code to the intermediate representation.
For a change like this, since it changes many symbols and Roc source code, the intermediate representation is very likely to change as well.
I think I get the basics. Not sure if I could replicate though :sweat_smile: but it doesn't matter for now
I think I changed everything
Should some encoders be updated? I remember one of the reasons for the opaque bool types was to make it so encoders don't have to special case the True and False tags
So do you think I have to change any encoder/decoder code?
if ROC_SKIP_SUBS_CACHE=1 cargo run -- check crates/compiler/builtins/roc/Json.roc passes you should be good!
They did!
Nice :+1:
I will test everything now, so that will take a while. If that succeeds, I will submit the pull request
Thanks for all the help!
definitely couldn't have done it without you :heart_eyes:
Noo
still some errors in gen-llvm
thread 'gen_primitives::increment_or_double_closure' panicked at 'Symbol #UserApp.b is not defined in environment {#UserApp.23: ValueId(6), #UserApp.two: ValueId(2), #UserApp.one: ValueId(1)}', crates/compiler/alias_analysis/src/lib.rs:606:21
failures:
gen_primitives::increment_or_double_closure
gen_primitives::mutually_recursive_captures
gen_primitives::unresolved_tvar_when_capture_is_unused
gen_tags::join_point_if
Is this updated on the branch?
yes
the error in gen_tags is a different one
failures:
---- gen_tags::join_point_if stdout ----
thread 'gen_tags::join_point_if' panicked at 'internal error: entered unreachable code', crates/compiler/mono/src/ir.rs:3925:30
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
the problem in gen_primitives seems to be that for some reason boolean variables can't be captured in closures
I have no idea what the solution could be though
Yeah, it's kind of a painful error. I think I have a fix, but verifying it now.
I have a fix for the join_point_if one. I'll send a PR to your fork for both when it's ready
awesome
thanks!
https://github.com/KilianVounckx/roc/pull/1
merged it
should I still run all the tests or have you done that?
I ran most of them, I think it should be okay
okay
Then I'll submit the pull request to the roc repo!
Thanks again
Thanks for doing this! Always great to have folks help out :grinning:
I hope to do more of it, but still learning :blush:
I see I haven't properly signed my commits again
I have no idea how though
I used git config --global commit.gpgSign true
and I had to enter my passphrase
is there something else I am missing?
Have you added your gpg key to your GitHub account? https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account
I did
@Kilian Vounckx your merge commit is signed but your earlier commits are not.
That's real weird, because I had to enter my passphrase on the first commit. Will try to fix it
As far as I can see, I can't go back and sign previous commits. Is there still a way to fix this? I'm really sorry if there isn't. Next time I will ask one of you before I make the first commit
if you can make a signed commit now, rebasing the branch in any way should make Git resign the commits.
I think I did it. Really not sure though, as I only use git for personal projects, so I don't know a lot about it. I rebased and pushed
@Kilian Vounckx Looks like something went wrong with the signing. No problem, git is hard :) .
To be sure I'd start with a fresh clone of your repo, the commands below should sign all unsigned commits:
mkdir newroc
cd newroc
git clone https://github.com/KilianVounckx/roc.git
cd roc
git checkout opaque-bool
git filter-branch --commit-filter 'git commit-tree -S "$@";' fe60ff83e646647b247273ab6acee0da0d176bc1..87c15d16ef9e89dd82aa2b9af0496f4e36a484d8
git push origin opaque-bool --force
just tried that
at the git filter-branch step, it says: You must specify a ref to rewrite.
Sorry, I think you need the short SHA's, so:
git filter-branch --commit-filter 'git commit-tree -S "$@";' fe60ff8..87c15d1
still the same you must specify a ref to rewrite :disappointed:
Hmm, I can look again tomorrow, I'll make sure to test it first.
@Kilian Vounckx the filter-branch commit needs the last ref to be HEAD so we can't use it here.
These are all the commands you need to execute for a rebase:
mkdir newroc
cd newroc
git clone https://github.com/KilianVounckx/roc.git
cd roc
git checkout opaque-bool
git rebase -i HEAD~14
# this will open an editor, change `pick` with `edit` next to the commits fe60ff8, 83019c5, ... up to(including) 87c15d1
# save than close the editor
git commit --amend --author="KilianVounckx <56159729+KilianVounckx@users.noreply.github.com>" -S
git rebase --continue
git commit --amend --author="KilianVounckx <56159729+KilianVounckx@users.noreply.github.com>" -S
git rebase --continue
git commit --amend --author="KilianVounckx <56159729+KilianVounckx@users.noreply.github.com>" -S
git rebase --continue
git commit --amend --author="KilianVounckx <56159729+KilianVounckx@users.noreply.github.com>" -S
git rebase --continue
git commit --amend --author="KilianVounckx <56159729+KilianVounckx@users.noreply.github.com>" -S
git rebase --continue
git commit --amend --author="KilianVounckx <56159729+KilianVounckx@users.noreply.github.com>" -S
git rebase --continue
git commit --amend --author="KilianVounckx <56159729+KilianVounckx@users.noreply.github.com>" -S
git rebase --continue
# you should see `Successfully rebased and updated refs/heads/opaque-bool.`
git push origin opaque-bool --force
so we've had Bool.true and Bool.false for awhile now...some things I've noticed about them that I hadn't originally thought of:
Eq implementations could change) ever considered exhaustiveBool.true instead of truethese reasons, plus the other reasons discussed previously (general weirdness budget etc.) are giving me a sense that maybe true and false should be reserved keywords like they are in most languages - and still be opaque, but participate in exhaustiveness checking
what do others think?
Exhaustiveness seems pretty conclusive. Having something as elementary as booleans defy the most basic expectations of not true => false is such a bad bit of jank that it seems worth fixing even if some people consequently overuse bools. Which itself, sounds like an issue that could be addressed with strong community norms and a section in the tutorial advising people on better alternatives.
Would true/false as keywords make json decoder logic any simpler? I seem to remember a conversation about that being awkward.
That was about using the tags True and False to my understanding. That said, I think introducing true and false keywords might demand a lot of special error messages just to tell people "you don't capitalise it in this language, however tempting it may seem".
yeah encoding and decoding behavior would be unaffected by this
I would much rather allow exhaustive pattern matching on opaque types as an opt in
As long as they exposed all wrapped values and are tags under the hood
We have talked about pattern matching on opaques on a few separate occasions, but never added support. It sounded like overall we wanted some sort of support though.
Personally, I haven't seen much of a reason for changing opaque bool from actual community comments of use cases. From what I can see:
Bool.something is written, but really not that often. I probably see it the most when it isn't even needed when a beginer does somsFn someVal == Bool.false. that can always be replaced by some sort of negation and probably isn't good practice anyway.true and false. It is maybe a gotcha once. Just doesn't really feel like a major issue to me.true and false at the top of any file and it would work just like a keyword.Pattern matching on bools is never needed [...]
Maybe all we need is a nice error message for when people try it.
Pattern matching on bools is never needed and probably is a bad pattern.
What about when the Bool is part of a larger structure? For example
isBread : Food -> Bool
isSandwich : List Food -> Bool
isSandwich = \layers ->
when List.map layers isBread is
[Bool.true, Bool.false, Bool.true] -> Bool.true
_ -> Bool.false
Tags would be more readable in my opinion: [IsBread, isNotBread, IsBread] ->
That's a fair use case for sure. I think a proper tag would be better, but I definitely consider the bool case reasonable.
I guess I was just thinking of bool and not nested bools.
That said, I think this example falls into "we should allow pattern matching on opaques if they opt into it", not that we should add a special case for bools.
timotree said:
isSandwich : List Food -> Bool isSandwich = \layers -> when List.map layers isBread is [Bool.true, Bool.false, Bool.true] -> Bool.true _ -> Bool.false
I think we intend to support this anyway right? Like I would expect this to be sugar for
[a, b, c] if a == Bool.true && b == Bool.false && c == Bool.true -> Bool.true
(I know, no boolean zen in that code)
Brendan Hansknecht said:
I would much rather allow exhaustive pattern matching on opaque types as an opt in
I think we should avoid doing this. It feels too complicated, and I don't really see the advantage it provides. The type is opaque; opaque types that are opaque just so that they can force the tag union to be closed, like Bool, are an extremely limited use case
I assume they would be opaque for other reasons. Like wanting a custom inspect impl or otherwise, but yeah, maybe too niche for the complexity.
I think we intend to support this anyway right?
I thought so.
yeah I agree with both of Ayaz's comments - I do think we should make that sugar work for pattern matching on opaques, but I think they should stay opaque and not be exhaustive
another option is to make Bool.true and Bool.false not keywords but quietly let them participate in exhaustiveness even though no other opaques work that way
I guess numbers are builtin opaques that actually do work that way (or should) because if I write out all 256 number literals for an 8-bit integer, that should be exhaustive :thinking:
thinking about this topic some more, I'm starting to think that the Bool.true and Bool.false experiment has suggested that we should just do true and false like most languages do. Reasons:
true and false because they're keywords) would be nicer.true and false, which describes almost all programmers who already know a language.Bool, now initializing it is more verbose than it would be with a tag, which actually incentivizes using the wrong tool for the job in order to get conciseness.I don't think the theoretical benefit of encouraging tag union usage has played out in practice the way I hoped it would.
so all those things considered, I think we should just do true and false keywords and have them participate in pattern matching the way you'd expect
what do others think?
As a beginner, I keep typing True and False and it often sort of works, but not quite. Bool.true is longer but unambiguous. I'd find lowercase true and false rather surprising because they look like they should be variables (although no reasonable person would actually use those variable names).
I guess we could instead make True and False special restricted tags
But that would probably be heavy handed in terms of compiler implementation
That said, I think I would prefer it over true and false. Would really make the language feel seamless around bools.
Bool is just [True, False], but True and False are special restricted tags that are only allowed to be used by Bool. Also, given True and False are only allowed in the Bool tag, Bool will never merge with any other tags and seeing just True or just False can still infer the type Bool.
I don't love the idea of special restricted tags. I think if true and false were language keywords no one would bat an eye, and then if you separately wanted to have a tag union like [True, False, Either]that would just work, no special rules.
I think people would question the when ... is syntax given it doesn't work with variables, but some reasons works with bools:
when x is
true -> ...
false -> ...
Also, technically we don't have to restrict the True and False tags, we could also just make it so that Bool is magically exclusive and will never merge with other tags. So you can still have [True, False, Either], but a Bool would not convert to that without an explicit cast.
That said, I think that would lead to complication is type checking cause it is unclear what to do with a raw True value and when to consider it exclusive.
I guess to be fair, even simply defining bool as [True, False] might be just fine. Any time it is used in a conditional, it would restrict to just Bool.
@Richard Feldman what are the issues with Bool just being an alias? Like it just being defined as [True, False]?
see the start of the thread! :big_smile:
ah yeah, issues with solo True and solo False.
Yeah, I definitely think I would prefer to just restrict the tags True and False and then use those as always boolean values.
Over true and false as builtin special variables.
so if you put True into the repl, what would it print as the type?
Bool
cause True and False can only be Bool. They are restricted from being part of any other tag
hm, so it seems like that idea is equivalent to (or at least indistinguishable from) True and False being the names of the special keywords instead of true and false, right?
in that they behave unlike everything else that's capitalized like that
sure, but:
Unless we allow pattern matching on arbitrary variables or opaque types, this will always look wrong:
when x is
true -> ...
false -> ...
that definitely looks strange today, sure
although maybe with syntax highlighting it's less weird:
when x is
true -> ...
false -> ...
This will look correct and wouldn't be question by users:
when x is
True -> ...
False -> ...
It would only get noticed if some user attempts to use True or False in another tag. In that case, we can easily print a good error message.
Either creates two special names. One in variable/function name space. The other in tag space.
The capitalized name just matches everything else tag union while the lower case name is just a bit quirky.
interesting, so:
True and False become language keywords that evaluate to opaque Bool valuesif we did that, we could do the same for Ok and Err and Result, which would mean Result could be opaque and therefore used with ! while still having pattern matching on Ok and Err work :thinking:
yep
since they'd be different from normal tags, should they get different syntax highlighting?
I guess they could get keyword syntax highlighting, but I think it would be fine if they just had regular tag highlighting...not really sure.
interestingly, that would also mean that putting Ok "foo" into the repl would print Ok "foo" : Result Str *
I wonder if having language keywords that look like tags might make tag unions harder to learn
because if I put True in it says Bool but if I put Foo in, it's [Foo]
(into the repl)
might be fine
I think this is a neat idea, but I'd still push back on the idea that users would find matching on true and false weird. Lots of languages use true and false as keywords, and even if they aren't reserved using them as variables like true = 1 wouldn't feel right. Pattern matching works on other concrete types so it's not like this would be an outlier.
interesting, so what would your preferred design be here?
I guess now that you mention it, Rust is a language that specifically does this
enum variants are always capitalized, variables are typically lowercase, but true and false are keywords you can pattern match on and I haven't felt it was weird
I guess that is fair, though I do think it is pretty weird that Bool would be the only opaque tag that can be matched against.
Roc isn't Rust. In Roc, Bool is a tag union.
In Rust, bool is actually a standalone type.
well bool isn't a tag union anymore
it's opaque, hence why we have Bool.true and Bool.false now
(because of the reasons at the beginning of this thread etc)
Idk, in my mind that just means it is an opaque tag union. So it is still a tag union and would be nicer to treat as one.
I guess in general, I don't like giving bool extra power that other opaque tag unions can't also have.
So I prefer to make it act like a non-opaque tag union so it fits with the power it is given.
what about the concerns at the start of this thread though?
like we originally had Bool as a type alias for the tag union [True, False], and that led to problems which resulted in making it opaque instead! :big_smile:
True. I guess that means that either way, Bool has extra power that is not given to normal tags.
Is there a reason we can't allow opaque tags opting into pattern matching in general?
yeah exhaustiveness checking
Can you expand on that, I don't understand why there is a problem
if opaque types can be exhaustively pattern-matched on, then that means you are no longer free to change the internal representation without it potentially breaking builds (due to things that were previously exhaustive becoming no longer exhaustive)
and maybe the most important characteristic of opaque types is being able to make changes to the internal representation without breaking things!
Sure, but this is opt in. I don't see the problem
I mean we could introduce a whole language feature where opaque types can opt into being pattern-matched on, but that seems like an unnecessary amount of complexity to add to the language just for True and False to be capitalized instead of lowercased :sweat_smile:
I more see it as a way to:
hm, but wasn't it only asked about in the context of builtins? :thinking:
maybe there are instances I'm not remembering
I guess if you wanted to be super flexible, you could make it an ability. The ability would auto derive to just unwrapping the opaque type. That said, if you want to hide part of the impl, you could change it to generate an arbitrary tag union instead.
I think it has come up a few times in beginner chats. Also came up in the abilities on any type chat cause it felt like a user had to pick between pattern matching and having abilities. This would get them both.
Is the idea that opaque tag types would (optionally) work with pattern matching, but still would not union with other tags?
yep
And I guess optionally hide info when being pattern matched on.
hm, I don't think that can work though
like let's say I put True into the repl
how is it going to know to print True : Bool?
like if any opaque type can do this, what's to prevent some other type from claiming the tag True?
and then how do you know which one "wins"?
do you have to have that type imported and in scope somehow?
if that's the case, then let's say I call a function which returns Foo and I try to pattern match on it - now that fails unless I import Foo explicitly
at which point maybe we also need to introduce the concept of "qualified tags" like Foo.Bar -> ...
Richard Feldman said:
what do others think?
FWIW I like the way it currently is, and haven't had any concerns just using Bool or Bool.true.
I don't feel strongly though if it changed, I could get use to it. But I do like the arguments for discouraging Bool and to use Tags instead. It's like a Roc superpower
Hmm. I was just thinking from the pattern matching perspective. I guess to initialize a value, it would still require a wrapping function or something else...not fully sure.
Cause when pattern matching, you have the opaque type as input, no need to determine which one it is. Just apply the ability that turns it into a matchable value
I guess with my idea, the only time you would see Bool.true is when setting the constant. (Like maybe the initial state of an accumulator)
Cause in matching you would use True.
I guess that might be more confusing cause you might see both the qualified name and the raw tag. Though I think in practice for most types that implement this ability, you would just see it in pattern matching and it wouldn't be a big deal.
how would that work with type inference? Like if I put this into the repl:
\foo ->
when foo is
Bar -> ...
what would the inferred type be of that function?
Hmm...I guess this is a case where it would auto-derive for standard tags and have a really ugly type. Though maybe it doesn't work at all (cause not sure how the output type of the ability would be constrained properly without higher kinded types).
...
Yeah, nvm, just seems too messy/potentially not really implementable. I guess let's just stick with Bool and maybe Result as types being considered for special treatment.
makes sense to me! So then the remaining questions for Bool are whether to:
true and false be keywordsTrue and False be keywordsBool.true -> and Bool.false -> be special-cased to be exhaustive in pattern matchesMy preference of the three is to have true and false be keywords. I think True and False would be confusing because they look like tags but wouldn’t behave like them.
Richard Feldman said:
makes sense to me! So then the remaining questions for
Boolare whether to:
- special-case by having
trueandfalsebe keywords- special-case by having
TrueandFalsebe keywords- not special-case the names, but have
Bool.true ->andBool.false ->be special-cased to be exhaustive in pattern matches
Is it outrageous to suggest an Ability that opaque types could implement that would allow pattern matching on them? I'm not quite sure what that would look like, but it would reduce the "for me but not for thee" aspect of built-in types. Records, tag unions, and lists can all be matched on, but there's no way to match on an opaque type without converting it into one of the above. For example:
when split iterator 2 is
([], _) -> ...
([a], _) -> ...
([a,b], rest) -> ...
or
when firstRest iterator is
Ok (a, rest) -> ...
Err _ -> ...
These are not terrible syntactically, but they aren't especially discoverable either. An ability could perhaps provide a standard way that the when statement could convert an opaque type into something it could already match on, or perhaps it could take some description of the match pattern and carry out the matching operation. Ideally,
when iterator is
[] -> ...
[a] -> ...
[a, b, .. as rest] -> ...
(without converting the whole iterator into a list, so that rest would remain an iterator).
we discussed this earlier in the thread :big_smile:
Last updated: Jun 16 2026 at 16:19 UTC