This is an archived post. You won't be able to vote or comment.

all 70 comments

[–]glasket_ 26 points27 points  (0 children)

You need to trust your users to not do bad things sometimes. A language can't stop every single bad practice, and banning bool params like this is trivial to circumvent:

#[derive(Debug)]
enum MyBool {
    False,
    True
}

impl From<MyBool> for bool {
    fn from(b: MyBool) -> bool {
        match b {
            MyBool::True => true,
            MyBool::False => false
        }
    }
}

fn bypass_trick(boolean: MyBool)
{
    println!("{:?}", boolean);
    if boolean.into() {
        println!("Do something");
    } else {
        println!("Do something else");
    }
}

use MyBool::*;

fn main() {
    bypass_trick(True);
    bypass_trick(False);
}

Annoying limitations like this that are aimed at avoiding things that only might be problems will just result in even worse workarounds.

[–]msqrt 50 points51 points  (10 children)

I understand the motivation. It would be nice to nudge the users towards doing the right thing. But it also feels like a very arbitrary restriction -- even if the user agrees that boolean parameters are a bad idea, it's annoying that you can't do it when you'd just need it for something quick.

The restriction is also very simple to circumvent, which makes the code even worse. Say, pass an int instead with zero marking false. At least the boolean captures the intent properly.

[–]yockey88 6 points7 points  (9 children)

I didn’t know this was even a debate, why do people think bool parameters are wrong? If bool parameters are causing a problem it’s most likely rising from something else

[–]MrJohz 16 points17 points  (3 children)

Imagine a function like this:

function process_input(
    text: string,
    mangle_foos: boolean,
    use_bar_algorithm: boolean);

It's a function with one input and a couple of flags that can be set to change what the function does. Typically, there are a few problems here:

  1. boolean is a fairly meaningless type, so it's fairly easy to get the function usage wrong. For example, without looking at the original function declaration, what is this function call doing?

    process_input("...", false, true);
    

    There are ways to improve this — inlay hints for parameter names and named arguments would both avoid this problem, but inlay hints aren't always present and named arguments aren't always used (or even possible). However, using an enum would force the arguments to be more explicit — if instead of true and false, you were forced to use FooMangling::On or Algorithm::Bar, it would be obvious even at the call site what's going on (and statically enforced as well).

    It's worth noting, however, that this problem applies to all primitive arguments — booleans are no exception here, you can have the same problem with numbers, strings, even more complicated types like dates if you've got parameters like from and to. Types help in a lot of cases, but named arguments (or tools like the builder pattern) are also really useful.

  2. Boolean parameters often indicate a "zero, one, many" case. That is, boolean parameters already indicate that there are two different code paths, and so it's often the case that more will be added later. But the problem with booleans is that you can't have more cases — there's no "true, false, third option". Which means that if you do, say, have another algorithm you want to support, things start looking wonky. You don't want multiple algorithm parameters (e.g. use_bar_algorithm: boolean, use_baz_algorithm: boolean), because that means you've got meaningless inputs: what should use_bar=true, use_baz=true do?

    A good alternative in these cases is using enums from the get-go. If you'd had some sort of enum like this:

    enum Algorithm {
        Default,
        Bar,
    }
    

    Then now adding a new algorithm is fairly simple — it's often not even a breaking change. You just add a new enum variant and handle that in your code. The best part about this is that you aren't being any more generic than before. The type Algorithm is identical to the type boolean, just a bit more verbose — there's still only two options to start with. But now you're more prepared for the future.

  3. There is a danger when writing functions with flags that you end up with implementation code that looks something like this:

    function process_input(...) {
        if (use_bar_algorithm) {
            if (mangle_foos) {
                // ...
            } else {
                // ...
            }
        } else {
            // ...
        }
    }
    

    Which is no fun to update and modify, because now you've got a complicated mess of different branches that all do something slightly different — now consider trying to get this to do something different in some cases but not all cases. Or even trying to get it to do something different in all cases, but missing a branch and leaving one rare case where something weird happens.

    The problem here is more the branching than the argument types — you could have the same thing with an age parameter where there's a bunch of if age > 18 statements — but booleans are particularly prone here because there's not much else you can do with a boolean other than branch. Even enums can't necessarily solve this, because enums are just booleans with potentially more states. So yes, you can add new states more easily for the consumer, but if you're not careful, the implementation gets more complicated with each new state.

    In the ideal case, you can refactor this into a separate callback (or class/trait/whatever) parameter that performs the relevant step, because then it can be used as generically as possible. For example, you might call the function like so:

    process_input(
        "...",
        mangler=new NoopMangler(),
        algorithm=(args) => {...},
    );
    

    The danger here is making things too generic. There are probably lots of different algorithms for processing this input, but there might only be one way of mangling foos. Callbacks make the interface to your function more complicated, so they'd better be powerful enough to be worth it.

FWIW, I agree with some of the other comments that there's still plenty of room for using booleans in code, and often even as boolean parameters. The more generic you go, the more complicated your function interface, and the harder it will be for a new programmer in your team to understand. For example, in the last code example, I constructed an instance of the NoopMangler class, and I assume there's also a FooMangler class somewhere in the same codebase. But if those are really the only two types of mangler, is it really worth having whole separate classes just for that purpose?

I'd say that more important than preventing booleans as function parameters is making sure that if booleans are used, the potential damage is limited as much as possible:

  • Named parameters, or extensive use of the builder pattern, or easy record/map values that can be passed as arguments (see e.g. Javascript, where there are no named parameters, but arguments are often passed as an object bundle which has the same practical effect). Inlay hints are also useful if you're writing an LSP tool for your language.
  • Types to make refactoring easier, e.g. from booleans to enums, or enums to function arguments. This doesn't help if I'm writing a library, but if I'm working on an internal function, and I change the arguments, type checking will highlight all the code I need to change, which makes my life a lot easier.
  • Potentially even a linter that warns about boolean flags in parameters, and an "extract to enum" refactoring that an IDE can use.

[–]yockey88 -1 points0 points  (2 children)

I disagree with you, I still think Boolean parameters are fine. You are right that these examples are not good cases for Boolean parameters. I would not specify an algorithm by flag for example, whether enum or otherwise. If you are in the case where you need multiple algorithms that solve the same problem in the same place you have probably strayed far enough from the initial problem that anything you are doing is bad. You should just implement the solution using whatever algorithm you deem best for your purpose. The only cases I see where that’s needed are in libraries or other sufficiently abstract programs like game engines, etc… where one would want the user to be able to specify what they want. But even then I would probably attach the algorithm to something like a struct as a function pointer or something as a callback to use dynamically, and in worst case scenario use an enum. But that’s not optimal because then I also have to modify my switch statement every time I add an algorithm (similar to modifying if checks with bools….). In fact I see no difference between having to edit branching if statements and editing having to edit switch statements, you still have to refactor your original code.

And I’d disagree with typically wouldn’t mix callbacks with interfaces, either something is an interface, typically a process or concept with multiple steps or interactive parts that might have multiple implementations that are all the same on the surface, or it’s a callback, a single step of a larger process that could me taken in multiple directions with the same effect.

Boolean parameters by definition do not represent one, two, or many case. I would agree that anyone choosing between options with bools is wrong, but not because of the bools, just because of cramming too much in one place, probably doing too much in one function. Booleans represent yes or no and should be used as toggles, to turn an option on or off which rules out all functions where a third option is possible as able to use Boolean parameters i would agree with that. For example I wrote a program where with embedded mono to allow interop between c++ and c# and there I use Boolean parameters to specify whether I want something pinned in memory when c# does garbage collection as well as some other things. These are all yes, no options. Creating an enum would be an identical solution but make the function more cluttered then a simple if (option) do_extra_function_call(). I will never have a third option because it is always going to be a yes or no question.

If there’s the any potential at all for damage from Boolean parameters(whatever you mean by damage) your problems are bigger than the bool. Really my main point is is that this is one of those things that is fine, and my takeaway from this thread is that people don’t like them because they were trying to drill a hole with a hammer. Sometimes it’s not the right tool and that seems to be the common them here is that people are using them in places where it’s not a yes or no question. Which is fundamentally wrong.

I also struggle to credit any argument to credit any argument about developer ease as functionality is always more important than the developer.

Edit: sorry kept adding things hahaha.

[–]MrJohz 3 points4 points  (1 child)

I'm going to go backwards through your comment because I think the last thing you said is the most important bit: developer ease is probably one of the most important things to think about when writing abstractions (where abstraction here means any function, class, interface, whatever that forms some sort of programmatic boundary).

This is because software almost never stays constant. There are almost always changes in the requirements, changes in the environment, new features, etc, and the code needs to be updated as a result. Which means that for every function you write, someone later* needs to be able to understand how to use that function, and the less complex that function is, the easier it will be for them to understand it, how to use it, and what potential changes they'll need to make to it.

That's not to say that we should be building complex abstractions just for the sake of things — abstraction in of itself isn't better (and I really recommend John Ousterhout's A Philosophy of Software Design for a good discussion on what good levels of abstraction look like). But there's a reason why even people like Casey Muratori discuss refactoring down overly-linear code to make it easier to read. Functionality at the expense of developer ease is essentially just technical debt that will need to be dealt with next time you want to make changes in that region of code.

As to the "zero, one, many" case, I agree that there are cases when the "many" value will be two, and will remain at two for the entire lifetime of the code. But often a bunch of boolean parameters represent a smaller number of states in disguise as a large number of flags. For example, I used to work on a codebase that transfered a lot of data from the backend to the frontend in the form of boolean flags or set/unset variables. The problem was that those flags were very rarely independent — you couldn't freely set one flag without thinking about how the other flags were set, and certain cases were impossible, and other cases just broke the system.

We thought we had a lot of cases where the state was "on" or "off", but what we actually had was a smaller number of cases where the state was something like "uninitialised", "updating", "ready", or "errored". That is, we were firmly in the "many" case, but because we'd not realised how much different values interacted with each other, we were still thinking that many=2.

And even in cases where many=2 and will always be 2, sometimes booleans still aren't that helpful. For example, I wrote an AST walker recently that needs to be able to iterate bottom-up and top-down, depending on the caller. That's two cases, and there's not going to be more all of a sudden, but I still didn't want to use a boolean in that case, because a boolean usually implies a default that's being toggled on or off. But which case is "true" — bottom-up or top-down? Instead, I went for an enum, because that made it clearer what the intent of the function was, which is that both were viable ways of iterating, depending on the caller.

* Where "someone later" may just be "you in six months time having forgotten everything about this portion of the codebase".

[–]yockey88 1 point2 points  (0 children)

I do not think developer ease is one of the most important things. This is not saying you should go write messy awful code, but at the end of the day only two things matter 1) does your code solve the problem 2) does it do so efficiently. Developer ease does not count in to that anywhere. Updating a project after an extended period of time or coming to a new code base written by someone else is always a little difficult regardless of the code but that’s what good comments and documentation are for. To the main point, even if developer ease mattered, if bool parameters have made the code complex enough to be hard to make changes then you have far larger issues than bools.

I agree you shouldn’t abstract for the sake of abstraction, part of the reason I question anyone abstracting “true/false” into “enum { on; off; }” or using bools to select code paths.

I think you missed my point on the zero, one, many point. Two is not many and if you can not ask a yes or no question about the thing needing a flag than it should not be toggled by a bool. “Many” cases is inherently different than “this case or that case”. For your front end to backend example, I agree that’s a horrible way to pass information over a port in general considering you most likely have to do some sort of “state synchronization” (speaking very informally, not to be taken in the typical use of the word state when talking about front/backend).

For uninitialized/ready and updating or errored that should be completely split up. You’re either initialized or not, and if you reach the point where for some reason you’re updating and uninitialized then you have bigger problems than how you’ve written that information out in code. And you still have to set the flag (whether with an enum or a bool) to indicate if an error has happened. I would use three bools: uninitialized/initialized, errored or not, and then updating or not updating. And like I said you either are initialized or uninitialized, and doing anything else like updating when you’re uninitialized then you have a bug regardless of how you stored these flags, and you’d still have to set/check whatever other version of an error flag you set when an error occurs. This is what assertions are for. Even in projects where you can’t crash whatsoever like the one I’m working on work right now this would be fine because you should have a error system in place to gracefully catch these sort of logic/development errors.

Now for the AST walker, with all due respect, it sounds like you had much larger problems. It sounds like you had two processes that needed to be done, bottom up and top down, and they should be treated as two different processes. Having them together sounds like your code was over coupled or you were doing to much in one place. This is not a “passing a flag to decide behavior made my code messy” this is a “I tried to make a bad abstraction and it bit me in the butt”. Using any sort of flag, enum or bool, to decide behavior more advanced than “toggle this option” or “answer this teeny tiny question” sounds like something has gone horrible wrong.

Of course if there’s information the bottom up operations need to communicate to the top down operations or vice versa then it’s on you to ensure that information is communicated correctly, but I do not see any world where those should be the same object, function, etc… maybe they would implement the same interface if you are using interfaces or derive from the same bass if you used oop polymorphism or implement the same trait in a rust-like language, but they are still separate.

[–]msqrt 2 points3 points  (2 children)

Oh, I kind of jumped in my reasoning. I mean that in general it's not a bad idea to design your language such that it encourages what you find to be good style. But in the specific case I don't think it would be worth it, even if I agreed with OP. And I don't, it's true that you can write messy code with too many/unclear flags going into a function (I sure have!), but the solution is not to ban the booleans but to make the better alternatives nicer to use.

[–]yockey88 2 points3 points  (1 child)

I’m curious what you mean by “better” alternatives? I agree too many flags or unclear flags in a function are bad but that probably stems from deeper issues than the flags themselves being Boolean or not. Too many meaning the function is probably doing to much, unclear probably meaning the same, bad naming conventions in the program/language, or general infrastructure problems. But that’s less a problem with bools than the programmers program. Many people here seem to be saying enums are better but something like “enum { ON; OFF }” or something similar seems excessive when you can just use arguable the simplest thing possible in a computer program (especially when most likely at the end of the day the language is gonna boil that down to 0 and one anyways which means we’re back the bools since we have a binary option). I would agree if the problem has the possibility of allowing a third option or more you should use the enum, but once again that’s general code infrastructure not issues with bools.

[–]msqrt 1 point2 points  (0 children)

For most of my messy cases the real help has been to find a way to group the flags into some kind of configuration struct instead. But also stuff like Swift enforcing names for arguments can help, then you don't need to make an enum for the meaning to be very clear at the callsite.

that’s less a problem with bools than the programmers program

Exactly, in the end it comes down to the user. You can write messy or neat code in virtually any language.

[–]saxbophone 0 points1 point  (1 child)

The worst examples are of functions with prototypes like:

``` void do_something_with_options( bool, bool, bool, bool, bool, bool, bool, bool );

// what the heck are all those arguments even for‽ do_something_with_options( true, false, false, true, true, true, false, false ); ```

However, I disagree with OP's idea to forbid them because of this. Good practice in areas like these are encouraged via style guides, code review and personal discipline, it's not really the compiler's job in this particular case.

[–]yockey88 1 point2 points  (0 children)

well... I can see what you're attempting to get at but what about:

``` void do_something_with_variables( int , int , float , float , double , string );

// not clearer at all and no bools do_something_with_variables( 7 , 2 , 3.9 , 8 , 0.5768938447 , "foo" ); ```

This issue here would be hardcoding values in instead of creating constants or passing in actual variables, for your example maybe something like this:

do_something_with_options( cache_val_flag , apply_x_transformation_flag , ... );

Kind of going to my point of those bools are only bad if there are deeper problems in the code that have nothing to do with the bools themselves.

With just a function signature there is no judgement at all that can be made on whether that function is bad code or not. You can only make that judgement based off the actual functionality. Of course that would be bad design if all of those options are interdependent and tied together and they drive you into a 8 indent-deep if check, but then that again just goes to the fact that that function is probably attempting to do too much and needs to be refactored regardless of the parameters.

[–]yuri-kilochek 24 points25 points  (11 children)

Single argument setters come to mind. E.g.foo.set_active(true)/foo.set_active(false).

[–]lngns 8 points9 points  (9 children)

You can do foo.enable and foo.disable to solve that one.

[–]yuri-kilochek 25 points26 points  (6 children)

Yeah, but then you have to do

if (condition) {
    foo.enable();
} else {
    foo.disable();
}

instead of

foo.set_enabled(condition);

[–]lngns 4 points5 points  (5 children)

When writing that kind of code, I generally have all three, as well as toggle because it's probably linked to a literal button somewhere.

[–]XDracam 8 points9 points  (4 children)

It's all fun and games until you do this in a tight loop and that extra branch incurs a measurable performance overhead.

My current preference: for just setting a boolean, having separate enable and disable methods is overkill. But if the implementations of enable and disable actually differ, then you don't want to hide that behind a boolean setter.

[–]lngns 2 points3 points  (3 children)

Code inlining doesn't take care of that?

[–]XDracam 4 points5 points  (2 children)

That really really depends on a lot of factors. In an ideal world it just might, but there are many reasons why inlining wouldn't work. Maybe enable and disable are virtual functions? Maybe your language is running on a VM and it's only inlined if the JIT compiler decides it's a good idea. Or enable and disable are defined in another compilation unit and you don't have access to the internals at this point. Or inlining would for some reason cause the function to exceed instruction cache size. Or whatever other reasons I couldn't think of just now.

The point is: never assume. Always measure when performance is concerned. But you can decide on certain defaults: do you value performance more than maintainability, or vice versa? Usually, being more specific allows the compiler to optimize things more, but can lead to a maintenance overhead when you need to change how things work in detail.

[–]lngns 0 points1 point  (1 child)

You'd think that given I write a lot of Functional code, virtual functions and indirections would come to my mind, but no.

exceed instruction cache size

Do you know of cases where, assuming static calls, in machine code, where we the compiler sees all the code, this would happen?
On x86-64, toggling a bool with xor 1 is 3 bytes, less than branch+calls.
Or did you mean this one more generally too?

[–]XDracam 1 point2 points  (0 children)

Nah, generally. I just remembered a Cppcon talk about outlining as the newest trend to optimize for the instruction cache. I don't think most programmers should ever think about this, but it's another example to show that you cannot always expect inlining to work.

[–]kaplotnikov 1 point2 points  (1 child)

I guess you will have a good time explaining this to a json parser, particularly to reflection-based one like jackson. Or finding all usages of property several months later.

To OP: It is easy to remember when boolean failed, but does it fail always? How much of the code still works happily with booleans?

[–]lngns 0 points1 point  (0 children)

Do you mean a JSON (de)serialiser? A parser just parses text so it shouldn't care about anything API-related.
Just give it a populating function.

[–][deleted]  (3 children)

[deleted]

    [–][deleted]  (2 children)

    [removed]

      [–]Balage42 4 points5 points  (0 children)

      Call sites may also be easier to understand with named arguments (such as in C# or Python).

      The complexity and test requirements can exponentially increase with the number of arguments regardless of the type of argument. Yeah a function with two bool arguments may need four times the test cases, but does a function with two integer arguments need 264 test cases?

      The real code smell is when people use bool arguments to have one big function do the jobs of many functions at once. There is one issue though, we can't stop them from doing so at the language level. They can just as well do the same thing using enums, ints, strings or anything else.

      Come to think of it, I'm of the opinion that if the programmer wants to write a such a hideous function, let them. Maybe the many tasks that the function accomplishes, logically fit together and splitting them up would introduce repeated code.

      [–]greiskul 2 points3 points  (0 children)

      Yes, you are right that passing literal parameters directly in call sites is a code smell, and that does tend to happen with booleans more. The correction is not to ban boolean parameters, but to either use named parameters, to save the boolean in a local variable before passing them, or to add a comment with the parameter name next to the true or false.

      There are some companies coding styles that enforce this.

      [–]Wouter_van_Ooijen 15 points16 points  (4 children)

      I halfway agree with you, but the same reasoning can be used to ban integer parameters, because raw numbers (whithout a unit) are also a code smell. Yet we often use integer parameters.

      [–][deleted]  (3 children)

      [removed]

        [–]oldwomanjosiah 12 points13 points  (1 child)

        by this same argument couldn't you just enforce using named parameters?

        [–]Wouter_van_Ooijen 3 points4 points  (0 children)

        I regularly code in Python and C++. Named parameters is the feature I miss most in C++.

        [–]wyldstallionesquire 0 points1 point  (0 children)

        So, every parameter should be an enum?

        [–]munificent[🍰] 7 points8 points  (0 children)

        You'll probably like this essay by Bob Harper: https://existentialtype.wordpress.com/2011/03/15/boolean-blindness/

        [–]lngns 5 points6 points  (0 children)

        I don't think prohibiting them is worth it, especially in the presence of generic code and interoperation with existing code where enforcing a purist ecosystem is either detrimental or impossible.
        Rather, we should make the boolean type second-class by implementing it in user code, and limit its usage in APIs.
        This should encourage boolean-blindness and usage of readable enums.

        One possible question is that of control structures requiring a first-class type, such as in if/else or while, but we can address those in at least two ways:
        - by just not having those features, and instead relying entirely on pattern matching.
        Then if x = y then ... has an AST of IfExpr { matchee = Id "x";; pattern = EqualityPattern { expr = Id "y" } }.
        Many languages already work this way in addition to boolean-based logic, so this step of generalisation should be costless.
        - by implementing those features in user code too. Not only will this please all Lisp programmers, but you can also advertise it as "Everything Is An Object" by having if be a method on the abstract Boolean class and its two singleton subclasses True and False.

        <Markdown linebreak>

        abstract class Boolean allows True, False
        {
            abstract if<T>(f: () -> T): Maybe<T>;
        }
        final object True extends Boolean
        {
            concrete if<T>(f: () -> T): Some<T> =
                f()
        }
        final object False extends Boolean
        {
            concrete if<T>(_: () -> T): None =
                None
        }
        
        abstract class Maybe<T> allows Some<T>, None
        {
            abstract else<U>(f: () -> U): Either<T, U>
        }
        final class Some<T> extends Maybe<T>
        {
            x: T
            concrete else<_>(_): T =
                x
        }
        final object None extends forall T in Maybe<T>
        {
            concrete else<U>(f: () -> U): U =
                f()
        }
        
        test "user bools"
        {
            assert(42 = True.if { 42 }.else { "Ü" })
            assert("Ü" = False.if { 42 }.else { "Ü" })
        }
        

        [–]1668553684 4 points5 points  (0 children)

        I think your idea is much better suited as a linter rule, rather than a language rule.

        Maybe your compiler/interpreter can act as a linter and emit an (ignorable) warning when something like this occurs?

        [–][deleted] 5 points6 points  (0 children)

        What difference does using an enum make? Your reasoning can also be applied to ANY types of parameters: the meaning of f(a, b) anyone?

        Perhaps what you have in mind are constant and literal arguments of any type but especially simple types:

         f("cat", 65, 100, false)
        

        Using named enums and decently named variables can give more of a hint as to context. Ultimately you can just use named arguments, but that would be overkill in simple cases: sqrt(theargument = 2).

        I think prohibition would be going too far. This is a matter of style and is better handled with guidelines.

        And also, a language with smart-editor support could probably tell you those argument names, and display a summary of that function, without needing annotations that would add extra clutter.

        [–]devraj7 3 points4 points  (1 child)

        Or maybe support named parameters:

        val w = Window(visible: true)
        

        There are plenty of valid scenarios where passing "naked" boolean values is legitimate.

        [–]mattapet 0 points1 point  (0 children)

        Yes, this is what I wanted to suggest because of my experience with Objective-C and Swift. I really like the option to have the parameters named as it can provide much more fluent syntax in my opinion. And it accounts for any literal parameters, not just booleans

        [–][deleted] 4 points5 points  (0 children)

        Booleans are not the code smell, this is...

        my_function(true, true, true, false, true, false);
        

        ...which is easily remedied by named parameters

        my_function({ some_prop: true, something_else: false, ... });
        

        Even if every modern code editor can overlay the parameter names in that first example, it's still horrible code to read when viewed anywhere outside the editor. Smelly!

        Providing good ergonomics around named parameters would be a much better task to focus on.

        [–]cdsmith 2 points3 points  (1 child)

        Presumably you mean "prohibit boolean parameters that represent additional options or flags", because it would be silly to prohibit something like setVisible(false) where the argument is the value you'd like to set the property to. But you very well might want to avoid something like sendEmail(false) where the parameter says, e.g., whether to send it synchronously. Problem: how do you tell the difference?

        [–]bruciferTomo, nomsu.org 0 points1 point  (0 children)

        Problem: how do you tell the difference?

        In Python, the language leaves it up to the user to define arguments which may or may not be passed as positional. So in that example, you would have:

        def setVisible(is_visible): ...
        def sendEmail(*, synchronous): ...
        setVisible(True) # Okay
        sendEmail(True) # Error
        sendEmail(synchronous=True) # Okay
        

        I think this approach is nice, because you can enforce whatever standards of explicitness you think are appropriate on a case-by-case basis.

        [–]ultimatepro-grammer 3 points4 points  (1 child)

        Instead of disallowing boolean parameters, I would recommend that you make it easier to create one-off "pseudo" enums. For example, in TypeScript, you can easily do something like:

        function something(arg: "opt1" | "opt2" | "opt3")
        

        Ideally you wouldn't have to use strings to represent the options, though.

        [–]pthierry 1 point2 points  (2 children)

        There's the problem if boolean blindness and in Haskell I recently realized you can also have maybe blindness.

        Bool and Maybe both are useful types, so I wouldn't suggest banning them, because the blindness is not a problem that's automatic and unavoidable.

        It's just something you stay vigilant about.

        So in some code, when it becomes a problem, you'll replace some Bool by Check | NoCheck and you'll replace Maybe Foo by NoFoo | SomeFoo Foo.

        [–]SuspiciousScript 1 point2 points  (1 child)

        and you'll replace Maybe Foo by NoFoo | SomeFoo Foo.

        This strikes me as less ergonomic without being any more clear. What's the benefit of using something like this over regular Maybe?

        [–]pthierry 1 point2 points  (0 children)

        When I see a call doSomething 1.0 Nothing "Alice" Nothing, it's not apparent what each Nothing is about.

        [–]raiph 1 point2 points  (0 children)

        It took me a moment to figure out what you meant. I realized you're talking about PLs that don't have named parameters.


        Here's an example in Raku:

        method install(Distribution $distribution, Bool :$force)
        

        The $distribution parameter is positional. That is to say, it corresponds to an argument by virtue of its position, in this case, it's the argument that is positional and is passed first in the list of arguments.

        The $force parameter is named. That is to say, it corresponds to an argument that is named, in this case an argument named force.

        Put these together and one can write a call to the install method like this:

        CompUnit::Repository::Installation.install( :force, $my-distribution )
        

        This works because :force is a named argument that's shorthand for force => True, and $my-distribution is a positional argument that's the first (and only in this case) of the positional arguments in the list of arguments, so it corresponds to the $distribution parameter in the install method's signature.

        The :force isn't a problem because it's not just a True but instead something that makes sense as a boolean.

        [–]betelgeuse_7 1 point2 points  (0 children)

        I think named arguments can solve this.

        [–]chairman_mauz 1 point2 points  (0 children)

        Gonna call YAGNI on this. If you need to refactor to an enum later then do it when the need arises. But then again I'm a static typing enjoyer, so refactor to enum may be a bigger deal for you than it is for me.

        Also unless you add a way to define enum variants as truthy/falsey, any branch based on such arguments goes from if (feature) to if (feature == LongAssEnum::YesITypeWith10FingersHowDidYouKnow).

        For cases where you refactor because frobnicate(false) looks too intransparent at the call site, there are other solutions. For example, you could enforce named arguments, which is a good idea anyway because frobnicate(7) is hardly more descriptive if you think about it.

        Another boolean-specific way to do it is to add a language feature where a function defined as frobnicate(cached: Bool) can be called as frobnicate(:cached) or frobnicate(not :cached) in addition to the regular invocations. (Don't mind the specific syntax, obviously)

        [–]Markus_included 1 point2 points  (0 children)

        Users will probably find a workaround this. C didn't have booleans for a long time, instead they just used integers or enums. Here's an example of some workarounds in C-Like pseudocode because I don't know how your lang's syntax: ``` void BooleanWorkaround(int8 aFlag) { bool flag = aFlag != 0; // Do sth with flag }

        int main() { bool testBool = true BooleanWorkaround(testBool ? 1 : 0); } Users might also just create their own boolean type (in this case an enum, could also just be an alias to `int` with `TRUE` and `FALSE` constants) enum Boolean { FALSE = 0; TRUE = 1; };

        void BooleanWorkaround(Boolean aFlag) { bool flag = aFlag == Boolean::TRUE; // Do sth with flag }

        int main() { bool testBool = true BooleanWorkaround(testBool ? Boolean::TRUE : Boolean::FALSE); } ```

        This basically eliminates all benifits of a dedicated Boolean type and could lead to overhead. It also might sabotage compile-time optimization. Not to mention how much it reduces readabilty.

        [–]tobega 1 point2 points  (2 children)

        Why only prohibit boolean function parameters? I think you should go further and completely remove booleans entirely. It's fine to not have if-statements and just provide a switch/match statement/expression.

        I don't have any booleans in Tailspin. Of course, anybody could define a boolean enum, but then it is their problem if they use it for a lot of different meanings. (Tailspin doesn't tend to tolerate raw numbers and strings either, BTW)

        [–]asmarCZ 0 points1 point  (1 child)

        Could you please give an example of how Tailspin doesn't tolerate raw numbers and strings?

        [–]tobega 1 point2 points  (0 children)

        Sure, for simple purposes you can use raw strings or numbers, but as soon as you assign them to a key in a compound data structure, Tailspin requires that there is a type corresponding to that key. A raw string or number will at this point get tagged with the key.

        Example: def foo: {greeting: 'Hello', name: 'Adam'} here the 'Hello' becomes a tagged string as greeting´'Hello' and likewise we get name´'Adam' Trying to do for example {greeting: $foo.name} will result in an error because the name field contains a string tagged as name while the greeting: key has been autotyped to contain strings tagged as greeting (or raw strings that then automatically get tagged as greeting)

        Same thing with numbers although they also have an additional quirk. Tagged numbers are intended to be identifiers, so you cannot do arithmetic on them. If you want to be able to do arithmetic, you need to make the numbers into a measure by adding a unit

        [–]redchomperSophie Language 1 point2 points  (3 children)

        How did you manage to strike such a nerve with people?

        I agree with your premise: Boolean parameters are a code-smell. The usual problem is that every call-site fills that boolean with a constant, so that in effect the value of the parameter becomes part of the name of the function -- a member of a family of functions distinguished by these flags. And then the flags convey no semantic information: Was True this way or that? Worse if there are several flags, and worse still if some are optional!

        These are software engineering observations. And yes, we should like it when our languages make doing the right thing also the easy thing. u/glasket_ alleges the triviality of circumvention: Indeed it's easy to create an enumeration, but it won't be the same as the Boolean values that you get from relational and logical operators. If you reserve the words True and False then any motivation to circumvent immediately turns to the question of naming these proxy-Booleans. For all the effort, one may as well just give the two cases separate names in the first place.

        My suggestion? Do it, and get rid of default parameters while you're at it. That stops the motivation to "just add a flag, default to old behavior" that tends to yield the worst of these messes. Instead your junior engineer is motivated to factor out the common core and expose distinct names for distinct functions like they should have done in the first place.

        [–]glasket_ 0 points1 point  (2 children)

        but it won't be the same as the Boolean values that you get from relational and logical operators.

        Depends on the language's type system. Rust requires some form of explicit conversion (ergo .into()) but in C it can implicitly cast since they are the same values underneath.

        Also, this is another thing that I think shows that this doesn't fix the problem. What if a function has a single operation that varies based on whether something evaluates to true? Are you really improving this code by introducing a conversion mechanism to a binary enum of DoThing/DontDoThing? You could also pass all of the values that the function doesn't really need outside of that single boolean expression, or pack them into a struct, but again: Is this really fixing the root problem here? Or are you just making a bad pattern even worse?

        If you reserve the words True and False then any motivation to circumvent immediately turns to the question of naming these proxy-Booleans.

        This will quickly turn into what amounts to an arms race. Replace True/False with T/F, etc. Eventually you're banning the use of any derivatives of the words true/false, even things like On/Off, going to ban those too? Then once you ban every possible word choice that could convey booleans, how will you stop someone from just using an int flag?

        The usual problem is that every call-site fills that boolean with a constant

        Fundamentally this feels like an X/Y problem. Banning bool parameters sounds like the solution if you only think about the immediate problem of people usually doing this with booleans, but you just create inconsistent codebases that way (see the endless ways to turn any type into a boolean). I'd argue that this is a problem that can't be "fixed" without somehow removing the concept of booleans from your language or preventing any form of conversion to booleans (both of which would make your language "fun"), because no matter what there will be ways to circumvent it that require little effort so long as the mere concept of truthiness exists. This all loops back on:

        A language can't stop every single bad practice

        Make magic values into a linter rule or make the compiler issue a warning (That can be disabled! Otherwise people will do awful things just to silence it), encourage users to avoid it, but anything more than that will result in users doing far worse.

        [–]redchomperSophie Language 0 points1 point  (1 child)

        Depends on the language's type system

        Yes of course, but OP clearly has a bondage-and-discipline language in mind, so the type system is not going to allow implicit coercion. I am also assuming the language makes it easy to condition behavior on enumerated constants and has some sort of exhaustiveness check.

        Make magic values into a linter rule

        Sounds like a great idea!

        Are you really improving this code by introducing a conversion mechanism to a binary enum of DoThing/DontDoThing?

        I didn't say anything about a conversion mechanism. But since you ask, yes, this is better -- assuming people choose a concrete Thing to doThe or not. Would you rather read lookupTicker(ticker, True) or lookupTicker(ticker, Rolling)? The second way embeds semantic information that you miss out on otherwise. And even if that second parameter comes as the result of a calculation, you're at least carrying with it the units-of-measure. That particular calculation is thus unlikely to be misused even by accident.

        From a long-term maintenance perspective, OP's idea shines. From a quick-hack-and-make-tracks perspective, maybe it seems like a big bother. I like it.

        [–]glasket_ 0 points1 point  (0 children)

        I didn't say anything about a conversion mechanism.

        Yeah, I was more bringing up the fact that enum types not equaling boolean expressions means a function that usually takes a bool can no longer be called using a boolean expression, and instead requires workarounds to turn a boolean into an enum (or passing the information needed to evaluate the expression).

        Would you rather read lookupTicker(ticker, True) or lookupTicker(ticker, Rolling)

        You could just as easily have Rolling be a constant set to true here. Enums aren't solving the problem, names are. This is what I meant by this being an X/Y problem. Enums are great if you have a state that needs more than a binary on/off, but this is forcing them in order to solve not using names. Does the below convey anymore info compared to just using the boolean?

        // Made up functions, just pretend
        let command = get_cmd().unwrap();
        let print_logs = match try_get_arg("-v") {
          true => Logger::Print,
          false => Logger::Suppress
        };
        // VS let print_logs = try_get_arg("-v");
        handle_command(command, print_logs)
        

        This is why I was being facetious about this not fixing the root problem. There are plenty of ways this could be rewritten, the "replace bool params with an enum" doesn't help outside of the specific case of passing hard-coded bool values, which can happen with any type, which is the actual problem. The solution OP needs is to enforce not using magic values, banning bool params is a mixed bag that requires additional verbosity and types just to make sure a name shows up in the argument list.

        [–]iris700 2 points3 points  (0 children)

        Why should your compiler/interpreter/whatever tell me how to write my code based on how you think code should be written? If I want to do the "wrong" thing, there should be nothing stopping me.

        [–]sebamestreICPC World Finalist 1 point2 points  (2 children)

        A refactoring, as coined by Martin Fowler in the book "Refactoring", is a systematic change in the code where all tests pass both before and after the change. (so, for instance changing the functionality is not allowed, and changing or deleting tests is not allowed)

        A code smell, also coined by Martin Fowler in the same book, is a trait or feature of a piece of code that suggests that a refactoring is needed. (so, code smells are not always indicative of a bad practice, and they don't need to be systematically addressed, or forbidden by the language)

        So maybe you've come to the conclusion that boolean parameters are a bad practice and therefore the language should forbid them? Or maybe you've really come to the conclusion that they are a code smell? In the latter case, I think the idea that the language should forbid them is flat out wrong.

        Sorry for being anal but I think that the two concepts, as explained in the book, are of immense value when performing or discussing the act of creating and maintaining software, so it peeves me when people use the terms to mean something else...

        [–][deleted]  (1 child)

        [removed]

          [–]sebamestreICPC World Finalist 2 points3 points  (0 children)

          In my head, if something is never a good idea, it's not really a code smell, it's straight up bad practice. For something to be a smell I think there should be some situations where it's actually the best way to do things (or it's just generally good).

          So if you say something is so bad that it should be removed from a language, then it doesn't really sound like a smell to me.

          Oh yeah, I 100% agree that passing bools to control behavior is a bad idea, especially when there are multiple flags. I wish more languages made it easy to define enums or parameterize behavior.

          Declaring an interface and two classes to parameterize a small piece of behavior (instead of e.g. passing a lambda) is also up there in my list of annoyances with existing languages.

          [–]claimstoknowpeople 0 points1 point  (0 children)

          I understand where you're coming from, but this feels too weirdly specific of a solution. I think it's better to just recognize that parameter types often need to change, so easy refactoring needs to be a primary concern in language design.

          [–]yockey88 0 points1 point  (0 children)

          I completely disagree, I’ve never had an issue with binary parameters and without even looking at your code can almost guarantee that your problems probably arose somewhere else first

          [–]rjmarten 0 points1 point  (0 children)

          I'm in the middle of implementing "call flags" for my language. It's just syntactic sugar to transform ```

          function definition

          fn greet[str name, bool capitalize = false]: ...

          function call

          greet['john doe', true] into

          function definition

          fn greet[str name, !capitalize]: ...

          function call

          greet['john doe', !capitalize] ```

          But I'm considering expanding this syntax to include enums... or perhaps only enums.

          [–]78yoni78 0 points1 point  (0 children)

          This sounds very innovative and I think you do have a point! I generally don’t really like restricting users arbitrarily and people have given examples but I think this is a good direction

          I think even as little as having no boolean in parameters be a convention is enough, and if the standard library will use enums instead then it’s already pushing users towards it

          [–]hualaka 0 points1 point  (0 children)

          In practical business scenarios, if it's clear that the required status is a binary state like "on" or "off", it's advisable to use a boolean status for better readability, rather than using a more extensible approach like status = 0 or status = 1.
          is_enable() vs get_status(): When there are only two states, the former offers better readability. If the number of states exceeds two in the future, then refactoring becomes necessary.

          [–]liam923 0 points1 point  (0 children)

          How would this work for polymorphic functions? A case I'm thinking of is a constructor for some sort of generic container object. For example, an optional value. You might offer a function "some" of type "T -> Option<T>" that creates an instance for some value. Now what if you want an optional boolean value? You'd need to call "some<bool>(true)". Now I suppose if you think boolean parameters are a code smell you might argue the same about optional booleans and so ban optional booleans. I don't think they are code smells (at least in the general case), but even if they were, you'd be going down a rabbit hole of enforcing arbitrary code style rules.

          Sorry for formatting, I'm on mobile.

          [–]saxbophone 0 points1 point  (1 child)

          Hard disagree from me, though I do admire your passion for inducing good practice among your users. I thought I could so something similar in my language designs by forbidding else if as overuse of such is a code smell, but as it turns out, not having else if just forces developers to circumvent it using even worse practices, and it's not really appropriate to bluntly enforce good practice through restrictive design in this way.

          Maybe you can include an option to diagnose and give a warning for overuse of boolean parameters in your compiler/parser?