Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Don't use booleans (2019) (luu.io)
102 points by pavel_lishin on July 8, 2024 | hide | past | favorite | 146 comments


Tangentially -- use languages that have great enum support.

Well you knew what was coming -- Rust enums are excellent[0], and so are Haskell's[1] (try and spot the difference between an enum and a record type!)... But that probably won't help you at $DAYJOB.

A bit more on topic though -- I'd like to see a strong opinion on Option<SomeEnum> versus SomeEnum containing a Missing variant. I usually lean towards Option<SomeEnum> but I wonder if there are any devout zero value proponents.

I don't like how golang does/requires zero values, but in more expressive languages I do waver sometime between Option<T> and T w/ a "missing" indicator variant inside.

[0]: https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html

[1]: https://wiki.haskell.org/Type


> I'd like to see a strong opinion on Option<SomeEnum> versus SomeEnum containing a Missing variant.

Java $DAYJOB here. If I'm a caller, I probably won't realise that SomeEnum.Missing exists. I'll just see that a function accepts a SomeEnum (which I don't have) so I won't call it.


This is a good point -- Option<T>is much better in a fn signature for comprehension. This probably clinches it.

Funnily enough, Java 1.8 is what really made me start taking Haskell and languages with better type systems more seriously -- seeing that Option actually deserved to be basically everywhere broke my frame.


An Option<Enum> has the advantage that you con chain it with other Option/Result operations like map, and_then, etc. I think this would be called being "monadic".

The advantage of Enum with Missing variant is that it's more expressive, and that you don't have to nest access in the Some matching.

Generally I prefer Option<Enum>, and often you can match directly with Some(Variant) to avoid nested matches.


> map, and_then, etc. I think this would be called being "monadic".

Strictly speaking, I think providing "map" just makes it functorial. Monadic would need a flatmap. (In addition to the other functor and monad requirements, of course.)


So what would you call something like "it implements some common operations", like these?

For example, the Option and Result type both have functions like "map", they do the same thing just on different types. They're not quite generic in that sense, but on a high level they seem so.

Another example are reactive libraries. Everything is pegged into some common operations like map, take, and so on.


You're right, and I'd add on that monads only need pure and bind (>>=), das it!

    pure :: Monad m => a -> M a

    (>>=) :: Monad m => M a -> (a -> M b) -> M b
pure is sometimes called return and >>= is sometimes called bind and flat map.

I was unsure that bind was the same as flatmap but it looks like it is.


The "and_then" that they mentioned is the flatmap


Option is the better of the two. With a missing variant, there's no way to encode "infallibly successful". All call sites would have to check for a missing variant in all cases.


This. Enum with a “missing“ variant is basically the billion dollar mistake again… kinda.


Option/Maybe/Result is a Boolean in disguise.

It has the same problem as Booleans by supporting only two branches: single happy path and single unhappy path, when real life may have many paths, and it’s not always clear which of them are happy or unhappy (from the business logic point of view).

Even for purely technical errors, simple OK | ERROR isn’t enough, but should be something like OK | RETRY | UKNOWN_ERROR | FAILED.


Yup, definitely a case for enums there -- I don't think the rule is absolute (I'm not sure you should always use enums over booleans), but much of the time it seems to be true.


Python's enum support[0] seems quite comprehensive, as well.

The nice thing about a class is that when some bizarre edge case comes up (and whose Extract, Transform, Load process has not?) one can isolate the weirdness.

[0] https://docs.python.org/3/library/enum.html


Typescript is great for this.

    function operation(user: User, state: “active” | “inactive”): void
Boom. Done. Enum defined. You want people to use enums? Remove all context switching from the definition process.


That’s the worst way to define enums. Now you have to make sure your codebase is using the same string literals everywhere and carry the potentially long type definition to every parameter, variable or function return type. And no, compile-time checks won’t cover 100% of cases.


Obviously if this gets reused you need to put it in a central definition.


How wouldn’t it be reused? Besides, what will happen if you need to change a constant in the enum from „a“ to „b“? Compiler won’t help you to spot all the places like ˋˋˋif (value == „a“)ˋˋˋ, it will quietly become unreachable in runtime.


You underestimate TypeScript. Your example will give a compiler error:

    This comparison appears to be unintentional because the types '"a"' and '"b"' have no overlap.(2367)
https://www.typescriptlang.org/play/?#code/MYewdgzgLgBAZiEAu...

It's features like these that make Python/Ruby/etc. type-checkers insufficient substitutes for TypeScript.

I think my example wasn't the best. Of course "active" | "inactive" would get reused. But if it's just a little function flag it could easily be a one-off.


The only place I believe this pattern is acceptable is option props in React components.

Otherwise, it becomes an absolute mess of “almost” defined correctly.


Ah I love Typescript (IMO JS is the best "scripting language" out there, and has been for a very long time, but that's a different discussion).

That said, I hate that typescript has many ways of doing it. That's the biggest problem.

There's:

    enum SomeEnum {
        First,
        Second,
    }

    const enum SomeConstEnum {
      A = 1,
      B = A * 2,
    }

    type SomeEnumType = "first" | "second";

    const SomeObjectThatIsAnEnum = {
        "first": 1
        "second": 2
    } as const

I always lean towards the `enum` keyword, because I am of the opinion that if you're optimizing for less generated JS (and enums are actually worth optimizing for in this way) you're probably using the wrong language to begin with (unless you're in the browser and have no choice, etc).


I've done things like this before: https://www.typescriptlang.org/play/?#code/PTAEAkFMBsAdIE4Gc...

The gist is: define your enum values as a const array. Then you can generate a TypeScript type based on whatever values you have in there. And, since it's an actual array, you can easily write guards / asserts.

So you end up with something easy to extend and also has compile-time and run-time checks.


Thanks for the snippet, it is INTENSE. Will see what I can learn from it — const array I definitely didn’t consider before!


TypeScript enums, from personal experience, should be avoided in cases where the value is stored.

The reason is that the compilation from `SomeEnum` to JS involves conversion to an object indexer[0].

Should you, in the future, need to (or accidentally) remove or change an enum option, this will fail with an index out of range exception at runtime that is not obvious to track down.

Put it another way: it leaves behind an artifact in the output JS that can be a source of exceptions.

[0] https://www.typescriptlang.org/play/?ssl=5&ssc=2&pln=1&pc=1#...


Sorry, can you explain this more? I somehow still don't understand.

How is this different from a const object style enum? Or are you more referring to the sum type enum as the better choice here to avoid this.

Generally, if you remove an enum variant, the compiler should warn you if you've used it anywhere else (though of course it can't figure out a dynamic usage)... But this seems like a problem mostly for dynamic code, where you'd probably write some sort of Enum.parse() or Enum.from() static method anyway?

If I'm understanding your comment correctly, the real problem is that when you store the value, remove one, and try to read it back out into the enum it will fail - but in the code I've written so far trying to construct the the enum object almost always consisted of using a function to validate anyway, these days I use zod but before I'd write stuff like:

    function isEnum(obj: unknown): obj is Enum {
      return obj && typeof obj === 'string' && Object.values(Enum).includes(obj)
    }


My understanding of the comment is that it’s in the context of saving an enumeration-object into a database.

Like Typescript might (for example) represent enums as integers like 1, 2, 3... at runtime.

What happens if you decide to store a “delivery status” enum in a database? You would just be saving a number into the database, which can be hard to understand.

If it’s a string, then the value stored in the database is clearer and doesn’t depend on you being lucky with the Typescript compiler that the same enum type is compiled to the same integers after new releases.

I only realised this issue now, but it’s what I understood after reading that comment.


This is also another problem and a reason to strictly stick to string type unions, IMO.

There may be a really good use case for the enum type, but I think in most cases, string type unions are clearer, easier, and less prone to errors.


Yeah, one of the annoyances I have with using Enums, even in other languages, is that its serialised value isn't always obvious. Something like direction.NORTH could serialise into "NORTH" or some integer, depending on the language and the implementation.

I like Typescript's union of string literals approach because its serialised value is never in question. I know exactly what "NORTH" serialises to.


In Java it is de facto standard to serialize enums as string, with exception of JPA where you can explicitly tell if it’s a string or a number. It also supports exotic mappings via explicit declarations. Typescript should have implemented enums with union type under the hood, enabling the use of constants instead of strings when passing/checking values.


    but in the code I've written so far trying to construct the the enum object almost always consisted of using a function to validate anyway
It's exactly this problem and it's not obvious for someone choosing an enum type that the underlying implementation in JS is an object indexer.


Typescript type system is probably gold standard, you can enforce so much statically


As far as gold standards go for typesystems... ML languages probably take it, with Haskell being the closest to widespread real world use.


It's the gold standard for languages you'll be able to use at work.


Same in Python, no?

def foo(bar: Literal["active", "inactive"])


What's the best Python type checker these days?

Seems like there are at least 4: Mypy, Pytype, Pyright, and Pyre


basedpyright:

https://github.com/DetachHead/basedpyright

(assuming you can't/don't use MSFT's pylance)


This goes away in a language that allows arguments to name the parameters:

  fetch(accountId, history = true, details = false);
However, I would be opposed if this mechanism is permitted to perturb the order of fixed arguments. If history is the third argument, rather than the second, it should error.

I.e. "history = true" means "we are passing true as the second parameter, which we believe to be called 'history', on penalty of error".

Fixed parameters having their names mentioned and checked should not be confused with the language feature of keyword parameters, which passes an unordered dictionary-like structure.


Is this an example from some well-known compiled language?

I work mostly in C++, and this is a feature I've wished for for a long time.




Nope!

> "When you use named arguments in a function call, you can freely change the order that they are listed in."

That violates a key requirement in my specification, which means that I regard it as broken.

Positional parameters should take arguments only in their designated argument position.

Only non-positional parameters (keyword parameters) should be allowed to be specified in any order.


Is there an example you have in mind as to why the params, if passed via keyword, would need to stay in their position? It sounds like external calls made to the internal api without the headers being present? Trying to understand why the position/order would matter to you once arguments are named?


If the code base supports designated initializers I'll use them as a way of getting named parameters like so:

struct SomeArgs { bool normalize{false}; int stride{2}; char ch{'x'}; ... some other stuff };

void someFn(SomeArgs args);

someFn({ .stride = 1, .ch = 'y' }); someFn({ .normalize = true });

not as simple as python but it's close-ish and more readable than: someFn(false, 2, 'c');


Swift and C# appear to have this feature, though they use the syntax

  fetch(accountId, history: true, details: false);


Objective-C. Swift.


Jai has this.


You can think of booleans as a built-in, two-value enum that has a common semantic meaning. Use booleans if this is suitable for your purpose; it will be suitable for many.

That said... this article feels like a bit of a strawman. Does anyone by default use three booleans in cases that are arechetypical enums?


It usually starts with "is_car", a simple, clear, yes or no. and then at some point someone needs to also check if it's a sedan and instead of turning it into an enum called vehicle_type an "is_sedan" is added, etc.


The other side of the coin is making an enum and eventually realizing that some of the types can overlap. I prefer the "set of tags" approach due to its flexibility.


Can't developers just... not do that? You can write bad code in any language. What is our responsibility as experts to avoid doing things like this, then?


if they know better and have the foresight, they do. But stuff like this might not occur to people who've never been bitten by it. Every step of the way you just make things a little bit messier than before.


It usually starts with one boolean.

Then people avoid touching it when expending a bit, so they just add another boolean for the corner case they're dealing with (potentially as an optional last param). And so on, until someone refactors the whole function bearing the burden to retouch all its legacy that accumulated up to that point.


My friend, I'd love to give you a tour of our codebase.


> You can think of booleans as a built-in, two-value enum that has a common semantic meaning.

I love it when a language's bool type is just a sum-type like the sum-types you define in user code, e.g. https://ocaml.org/manual/5.2/core.html#hevea_manual10. It's an indicator of a language with good foundations.


I agree. Nim’s booleans are an enum too: https://nim-lang.org/docs/system.html#bool


Unfortunately, yes


Oh gawd yes they do!


The antipattern in the article is also called "boolean blindness" sometimes.

https://existentialtype.wordpress.com/2011/03/15/boolean-bli...

https://duckduckgo.com/?q=boolean+blindness


It seems it's not so much booleans than using unnamed parameters for option parameters. I find myself using this style in C, it does not prevent a mix-up if someone refuses to use named field designators but it works ok :

    typedef struct options_s {
        bool toggle_case;
        bool strip_whitespace;
    } options_s;
    
    char *modify_string(char *str, options_s options)
    {
            if (options.toggle_case) { /**/ }
            if (options.strip_whitespace) { /**/ }
            return str;
    }
    
    int main(void) {
            char str[] = "Hacker News";
            modify_string(str, (options_s){ .toggle_case = true, .strip_whitespace = false });
    }

Now that I think of it it's probably trivial to forbid the use of struct literals without designated fields in code in a linter

Maybe we get anonymous struct function parameter declaration with C32 ? :D

EDIT: I have been asking around to people fluent in standardese and if you leave out fields in a struct literal you are guaranteed they will be zeroed-out


> It seems it's not so much booleans than using unnamed parameters for option parameters

It's actually using unnamed parameters at all. While this issue is more frequent for boolean parameters, it can happen for integers or floating point numbers as well. We do a lot of numerics in C, and calls such as

    optimize_foo (&state, 0.3f, 1e-5f, 1.f);
have the same issues as mentioned in the post: Error prone, more difficult to review, more risky to refactor.


If a giant meteor took out WG14 we might get named arguments in C

   modify_string(.str=str, .options = { .toggle_case = true, .strip_whitespace = false });


I've wanted a similar thing for C++.

Anyone know what the reasons have been for the language not adopting this?


It would cause argument names to become part of the function signature, which would be a significant change to the language, and its ABI.


Wouldn’t you be able to treat this as syntactic sugar - effectively you rearrange the arguments as needed and strip the labels. Doing that wouldn’t necessitate any kind of change to the ABI, I think.


Yes it's basically designated initializers for function arguments. Interestingly designated initializers got added to C99 and then WG21 sat on their finger for 20 years before adding it to C++.

It can't be anything but the most trivial thing to implement.


Folks, the headline is real bad and clickbaity, but I think the blog post is actually very, very correct in its basic premise.


Totally agree - I appreciate and understand (and agree with) the enum idea.

But stating “don’t use booleans” is a terrible absolutist ideas that will lead to terrible code. Imagine a student reads it, takes it to heart, and starts creating dedicated enums for every logic branch in their code.


Tangential but coding in JS/TS, I often will go for object arguments to make things more readable. If you have a function like:

foo(arg1: boolean, arg2: boolean, arg3: boolean)

Then when you call it it will look like foo(true, false, true) which is not great for readability. Instead I move all the arguments into an object which makes each field explicit. Ie.

foo({ arg1: true, arg2: false, arg3: true })

This also carries the advantage that you can quickly add/remove arguments without going through an arduous refactor.


you can force named arguments in python by using * after any non-named args. example:

    In [1]: def my_func(first, *, second, third):
       ...:     print(first, second, third)
       ...:

    In [2]: my_func(1, second=2, third=3)
    1 2 3

    In [3]: my_func(1, 2, 3)
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    Cell In[3], line 1
    ----> 1 my_func(1, 2, 3)

    TypeError: my_func() takes 1 positional argument but 3 were given

    In [4]: def my_func_2(*, first, second, third):
       ...:     print(first, second, third)
       ...:

    In [5]: my_func_2(first=1, second=2, third=3)
    1 2 3

    In [6]: my_func_2(1, second=2, third=3)
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    Cell In[6], line 1
    ----> 1 my_func_2(1, second=2, third=3)

    TypeError: my_func_2() takes 0 positional arguments but 1 positional argument (and 2 keyword-only arguments) were given


You can actually set up VSCode and probably other editors to show the parameter names inline with the values.


Same for JetBrains IDEs.


I hate inlay hints because they usually add unnecessary clutter. I prefer to use languages that allow you to add them manually when needed.


How is this done for js?


As far as I know it is not a native feature, but inline hints for parameter names can be added with a VSCode extension called "Inline Parameters for VSCode": https://marketplace.visualstudio.com/items?itemName=liamhamm...

The only languages it supports are JavaScript, TypeScript, PHP, and Lua. I can confirm that it works for JavaScript and TypeScript, I haven't tried the other two.

A similar extension named "Inline Parameters Extended for VSCode" supports a different set of languages: Golang, Java, Lua, PHP, and Python. It seems to be a fork of the aforementioned.


One of the things that's often not considered with these kind of interfaces, is whether or not they're actually possible. For instance, arg3 might only be true if arg1 is also true. Or arg3 may not be false if both arg1 and arg2 are also false.

Using object arguments is a great start, and I think using enums can also be powerful (if you're using string literal types). But often times I reach for explicit interfaces in these situations. IE:

type FooScenario = { arg1: true; arg2: true; arg3: true };

type BarScenario = { arg1: false; arg2: true; arg3: false; }

type Scenario = FooScenario | BarScenario;

...etc

This provides semantic benefits in that it'll limit the input to the function (forcing callers to validate their input), and it also provides a scaffold to build useful, complete test cases.


Big time.


I interpreted this as 'don't write crappy functions taking confusing arguments'. If the function had taken 4 floating point numbers, the argument would be the same - it would be confusing to know what is what.


Sure, but booleans are the lowest hanging fruit, most programmers won't write many functions which take 3 floating point numbers and are then modified to add a fourth but they will write functions which have three flag booleans and then add a fourth.

The generalization of the author's lesson is the New Type idiom. Don't use basic types like "integer" when you can invent types which correspond to the actual meaning of your values. A Duration, a FileDescriptor, a DatabaseHandle, a KeyboardScanCode. Once we do this it's obvious that if the function takes a FileDescriptor and a Duration we should not give it this KeyboardScanCode we have twice, that's clearly nonsense.

Rust piles on extra value for its own New Types (as yet you cannot directly grant this to your own types although if they're enumerations they get it for free) - niches. Result<(), OwnedFd> is the same size (a single 32-bit integer) as the C int you'd have used for this purpose in a much less capable language, but since it's a Result type it has all the same ergonomics as if it was much more complicated and heavyweight than that.



Solution in C#: use named parameters: RaiseInvoice(taxInvoice: false, sendEmail: true)


If you don't have named parameters, you can fall back to variables

  var taxInvoice = false
  var sendEmail = true
  RaiseInvoice(taxInvoice, sendEmail)
It's obvious, but people tend not to do it.


Sadly there is nothing that can tell you that you got those arguments backwards.


Point taken. But you can check the declaration of RaiseInvoice and see if the names match. If you had RaiseInvoice(false, true) then you really wouldn't know.


Totally a matter of taste, but for an equal level of protection and less vertical waste I’d rather just do

    RaiseInvoice(/*taxInvoice*/ false, /*sendEmail*/ true)


Which is far more realistic than defining single-use enums all over the place.

I don't know why more languages don't have named parameters...


Why is defining an enum "unrealistic"? It's at most 4 lines of very simple code to define an enum that replaces a boolean.


Think about cases more complicated than passing a bare literal.

  // Booleans + Named Parameters
  foo(should_scrub=not options.scrubbing_disabled)

  // Single-Use Enum
  import ScrubbingOption, ShouldScrub;
  foo(
    if options.scrubbing == ScrubbingOption::Disabled {
      ShouldScrub::No
    } else {
      ShouldScrub::Yes
    }
  )


If you format the second example more sensibly:

    foo(options.scrubbing == ScrubbingOption::Disabled ? ShouldScrub::No : ShouldScrub::Yes)
it doesn't look much worse at all


I'd actually go further and say that there's no reason I can think of that foo() shouldn't have access to the ScrubbingOption, so just pass that in.

If you have two-option enums and for some reason you transform them into two-option enums that represent essentially the same thing, of course that's going to be unnecessarily complex.

As a general rule, "it's more verbose" isn't much of a concern to me: verbosity is a very poor indication of complexity, and entering code with your keyboard simply isn't the bottleneck for writing code (if it is, you aren't thinking about what you're writing enough).


> If you have two-option enums and for some reason you transform them into two-option enums that represent essentially the same thing, of course that's going to be unnecessarily complex.

"For some reason" being anything so prosaic as they come from different libraries.


If you have two libraries that aren't explicitly made to work with each other and operate on the same problem domain, being explicit about the translation layer between those two libraries is very much a feature, not a bug. That's absolutely a spot where verbosity and named enums will shine.


That doesn't fix readability (is the invoice being raised not a tax invoice? Or are we not taxing the invoice being raised?).

That doesn't fix the possibility of adding a third possibility either (what if we separate non-tax invoices into two types of invoices?).

RaiseInvoice(InvoiceType::Tax, Notifications::Email);


no, way better to make an options object then set the options you want, also works a lot better later when you want more options.


Way better use a bit pattern.


You can't force the caller into using named parameters though. So when it's late at night, you're tired but also think you know better, there's nothing stopping you from doing RaiseInvoice(true, false) when you actually meant the inverse.


Downsides: you have to name and import all the enums (notice no one ever shows this in code examples). You can't do boolean math on them (and, or, etc.).


In most languages with strong types, the IDE will handle imports for you pretty easily. Selecting an import with a hotkey isn't as hard as debugging a mysterious boolean.

Not being able to use logical operations on them is a feature, not a bug: logical operations on unnamed values here are going to be even less readable than passing them into functions.


The IDE, like IntelliJ, can tell you which boolean has what name without named parameters.


The assumption being that the language you are using does not have named parameters. Something all modern languages have, except, as usual, the ones the world is built on (JS, Java).


I got tremendously sad when I watched Guy Steele's talk growing a language:

https://www.youtube.com/watch?v=_ahvzDzKdB0

It's one of the best programming talks I've come across. For 1998, all his points on growing a new programming language are spot on.

And then Java became the opposite of a lot of the goals he enumerated.


Named parameters are easy enough to mimic in JS using objects and the spread operation

function foo({bar, baz}) { ... }

foo({bar: 1, baz: false})


I'm aware, but "mimic" is the key word here. Other languages are named-first. E.g. in Swift nobody is thinking "should I name this parameter?". They name it by default, because they have to choose an internal name anyway, and they only reach for the anonymous-parameter feature after explicitly considering "should I make this parameter anonymous?"

In other words, this blog post is practically irrelevant to those languages.


I don't agree. One can easily imagine how the "a bool for every edge case" approach won't scale for long when extending an existing code base. Regardless if the language or IDE makes you see the names of the booleans or not. I think the author's point is not so much about the names but about abstraction and concepts. Slapping another bool parameter onto a function and bifurcating its behavior with an `if` is easy to do, but hard to refactor once this has been done a couple times to that function.


Lemme be an absolute pedant and say that's destructuring, not spreading.

And correct me if I'm wrong, nothing worse than an incorrect pedant.


I'm not sure, but I think homicide might be worse than an incorrect pedant.


Well done.


Is the so-called victim the pedant in question?


that's its own kind of horrible though. now either all your functions just take single object parameters or you have to remember which ones. and I get javascript is light on checking what was passed is actually what was expected but this seems to somehow make that situation even worse still


Never had an issue with “remembering” part. At first I hated js/ts for its runtime and legacy quirks, but shortly after getting used to them it struck me that it’s a practical language with as little religion as possible. They just look at something and make it useful. Without thinking how much horrible it could be if they only imagined it.

The ([…, ]{…}) approach is called “options object” which is a separate entity and is very convenient compared to e.g. python’s semi-infinite arguments lists.


TypeScript is the answer.


Is there anything wrong with taking a single parameter?


This is a fairly common pattern but it suffers from a big problem in that the compiler usually can't check your parameters for correctness. If you build a struct with the parameters but accidentally misname one that error is hard to catch.

My ideal language would have function prototypes that look something like:

    return_type function_name(parameter_name type (default_value) [constraints], ...)
If the default value is not set then the parameter must be passed or it is a compile error.


JS/TS has a pretty widespread convention for "named parameters":

  function foo({x, y, z=1}) { ... }
It eschews positional parameters though, but makes passing aptly-named variables as arguments easy:

  const x = 1;  // Usually a complex computation instead.
  const y = 2 * x;
  return foo({x, y});


Having named parameters is such a revelation. And what do we get instead? Added features that make the language more complicated.


Or the sorta annoying, un-ergonomic builder pattern: https://www.baeldung.com/java-builder-pattern


If the method takes so many arguments that named parameters is necessary for readability, it is often a sign that the method is either too complex and should be split up, or that the method should accept a "config" record as a parameter. I this makes it clearer because you can group the parameters and give them meaning. Then you know that "url", "headers" and "body" belongs to the "request" record, while "customerId" and "taskId" are separate


And Go :(


Fortunately you can use an options struct which retains most of the benefits of named parameters


Yes, use booleans. Why would I want one more layer of abstraction staring at me mocking my memory?


Your brain's memory, or your computer's? If your compiler isn't handling enums efficiently it may be time to get a new compiler.


I feel like this comes up here and there, and generally what I think is these probably aren't good functions. Enums won't save you from all the twisting and turning you're doing in a function that takes 3 booleans.

Rules like this aren't that useful IMO. What if you have like a SetActive(bool active) method? Does this also get an enum? Should this be two methods? Blah blah blah. You can only cultivate taste with experience and consideration, not with rules. Everyone's seen codebases that followed all the rules but were still total messes. Or as Tool said: think for yourself; question authority.


> What if you have like a SetActive(bool active) method? Does this also get an enum?

I'd be inclined to do away with the argument entirely, instead going with separate SetActive() and SetInactive() methods.


>> I'd be inclined to do away with the argument entirely

> Should this be two methods? Blah blah blah.


I haven't seen it mentioned elsewhere in this thread, but I really appreciated my IDE adding extra information in function calls, annotating create(true, false) into create(initial_state: true, reset: false)


One more benefit to favor enums for booleans here. When passing facts/intent solely through a generic true/false, and possible a single more or less well-named identifier, you have increased risk of multiple and differing interpretations of what the values mean: Did 0 or 1 mean failure or success? did false mean that the file did not exist or that you are not allowed to access it? By explicitly naming what your 'true' and 'false' cases mean, you _can_ lessen this risk. (Of course, you can still communicate badly both with booleans and with badly named enums.) Also for return values for functions, you don't have an identifier name (not all acting functions share name with their "report-back"). Further, enums _may_ upgrade elegantly when you learn your boolean assumption had >2 cases.

I read about half of of submissions, I don't know if others also said all this already.


caveat: In languages where enums silently accept random ints, they break this somewhat, sadly.


Keyword arguments or named parameters solve this. In JS I tend to pass a single object to a function with a large number of parameters like this. But I do agree using named entities like enums or constants is also good. A bit heavier but if you do it everywhere the cost is worth it.



I'm definitely a fan of making bad data 'unrepresentable' through the type system. The bit flag enum pattern is not so familiar to me in Python, but I recognize it from the CPython internals so maybe it's more common in systems languages. Not sure how I feel about explicitly listing every valid combination of flags, but I'm sure in some cases that's the right move.


Yes, 100%. Although you end up with some super gnarly types sometimes, so much better than marking a bunch of properties as optional but not handling certain combinations of them being passed in correctly.

Lord knows I still do that most of the time though...


In Python you can use keyword arguments to the same benefit.


One thing that annoys me about enums is that I'm always afraid that I'll write code like

    @enum MyEnum A B
    sometest = x == A ? B
Which is correct but that one that I'll add new elements to the enum

    @enum MyEnum A B C
and forget to check if that code is still correct. Suggestions?


Pattern-match over them and don't use a catch-all clause. Let your compiler emit warnings for unhandled cases.


Elixir's approach is great imho : https://hexdocs.pm/elixir/keywords-and-maps.html


fetch( accountId, true, // include disabled, true, // fetch history, true, // fetch details );

typescript saves the day again

fetch(accountId: AccountId, options: {includeDisabled?: boolean, history?: boolean, details?: boolean})


That doesn't really solve any but the first problem mentioned in the article.


I agree...mostly.

Of course, doing this adds overhead to the developer at the time the code is written. Instead of just writing 'bool', you need to go into the header and define a new enum and decide what it and each of its entries will be named, and make sure that it is propagated up through any dependencies that will interact with that function and so on.

Frankly, that can be a lot more work - especially in a large legacy code base.

Sometimes just a simple Bool is easier.


The beauty of booleans is the clarity and simplicity.


Got it.

use_boolean = false;


thing_to_use = enums; // fixed


Basic style guide stuff.


use enums instead

type boolean = (true, false, EFileNotFound)


Love it. Simple, straightforward advice. Thank you.


I’m reminded of what a beautiful language objective C is. It completely solves this problem. For example, take a look at this iOS method name (real Apple API):

initwithenablefan:enableAirConditioner:enableClimateControl:enableAutoMode:airCirculationMode:fanSpeedIndex:fanSpeedPercentage:relativeFanSpeedSetting:temperature:relativeTemperatureSetting:climateZone:

/s


Tell me you're almost ready for Rust without telling me you're almost ready for Rust.


You're not wrong, but you're probably getting downvoted because you didn't elaborate on what you mean. Possibly also because the way Rust handles structs isn't exactly unique to rust. Go does it, Typescript does it, and so on.


Happy to elaborate. Although I was having enums in mind https://www.youtube.com/watch?v=Epwlk4B90vk




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: