Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: math: add Bool(bool) int #61915

Closed
jimmyfrasche opened this issue Aug 9, 2023 · 24 comments
Closed

proposal: math: add Bool(bool) int #61915

jimmyfrasche opened this issue Aug 9, 2023 · 24 comments
Labels
Milestone

Comments

@jimmyfrasche
Copy link
Member

Helper to convert boolean expressions to numeric types under the standard false = 0, true = 1 mapping.

package math
func Bool[T number](v bool) T {
  if v {
    return 1
  }
  return 0
}

where number is replaced by the constraint for all numeric types, wherever that ends up and whatever it ends up being named.

As inference improves, the type should need to be specified less often.

I don't think the name is too important at this point. The important parts are is this the right thing in the right place. Names can be discussed once those are decided.

A language change to allow direct conversion from a boolean expression to a numeric type has been rejected several times, #9367 and #45320 . This is the next best thing.

This has been forked from discussion in #61643.

@ianlancetaylor
Copy link
Contributor

We can't infer the type argument, and I would imagine that int is a common case, so I think this ought to be

func Bool(v bool) int

That will save people from writing Bool[int](b); they will write Bool(b) instead. People who want a type other than int can write int64(Bool(b)), which is no worse than Bool[int64](b).

@atdiar
Copy link

atdiar commented Aug 10, 2023

Same reflexion as @ianlancetaylor and I also find the function name confusing.
No idea what could be better.
math.BtoI? math.BoolInt? Etc.

@jimmyfrasche
Copy link
Member Author

@ianlancetaylor you're probably right but just because it's not inferred now doesn't mean it will never be inferred.

If inference is later changed so that the generic version could be used like

var x int64 = math.Bool(p)
n := x * math.Bool(q)

the non-generic version would still need to be used like

x := int64(math.Bool(p))
n := x * int64(math.Bool(q))

Even with maximal inference I imagine there would still be times where you'd need to write the type. This is most useful as part of another expression so I'd have to assume that there'd be enough type information the majority of the time in practice.

I wouldn't mind adding the type even if it's most often int but it would be nice to have to do it less and less as inference improves.

Even today I'm sure you could find cases where it would be handy to have the generic version like passing it to something like

func f[S, T any](x S, y T, f func(S) T) S

I'm not sure what the best long term decision is but I'd hate to sacrifice it to what's optimal now. That said, I'd be fine if it were int only. That would probably be good enough most of the time.

@atdiar
Copy link

atdiar commented Aug 10, 2023

@jimmyfrasche since it's a function call, the general handling of inference would require interprocedural analyses (the inferred type can depend on the call site which can be found at an arbitrary depth)

Even without that, I'm not sure inference will work for every calls.

I'd tend to think that int would suffice.

@jimmyfrasche
Copy link
Member Author

@atdiar consider the example of n * math.Bool(q) the information available in that expression is

  • n has known type T0
  • * needs both of its arguments to be the same type
  • math.Bool[T1](q) has unknown type T1

From that we can conclude that T0 = T1 for the program to be well typed.

Whether that can be computed efficiently and whether it gets implemented are different questions but it's not anything fancy.

Certainly that doesn't mean every case imaginable could be inferred but my assertion is that most calls would be within an arithmetic expression so if those are supported that should cover a lot of the uses.

@atdiar
Copy link

atdiar commented Aug 10, 2023

@jimmyfrasche

I see what you mean. But then for comparisons,

if math.Bool(p) < math.Bool(q){...}

Wouldn't work, no?

(*using the math.Bool name although I still think that it is confusing since it doesn't return a boolean)

@earthboundkid
Copy link
Contributor

Maybe strconv.Btoi makes more sense than math.Bool. If it were strconv.Btoi() string with outputs "0" and "1", you could still compare the strings with cmp.Compare.

@earthboundkid
Copy link
Contributor

The other idea is to have package bools, something like

func Int(bool) int // 0 or 1
func Compare(bool, bool) int
func Xor(bool, bool) bool
func Xnor(bool, bool) bool

@jimmyfrasche
Copy link
Member Author

the name: unimportant for now. No point in finding the perfect name if it doesn't happen. It's a placeholder. Feel free to continue to discuss it but I am purposefully ignoring my impulse to engage for now.

@atdiar you are correct that would always require a type to be specified for at least one of the calls.

@carlmjohnson:

re strconv.Btoi() string comparison isn't the only use case. A more important one is using it as an https://en.wikipedia.org/wiki/Iverson_bracket

A package is interesting. It could contain Int and a generic version, for that matter. Maybe Any and All as well. I like that idea.

@earthboundkid
Copy link
Contributor

Maybe func Any(...bool) bool and AnyFunc[T any](func(T) bool, ...bool) bool, plus All/AllFunc. Could also be called "Some" and "Every".

@jimmyfrasche
Copy link
Member Author

Should probably take an iterator or at least have iterator variations. Similarly everything should all work on [B ~bool].

@dsnet
Copy link
Member

dsnet commented Aug 14, 2023

I'd argue against this being a function.

Rather, this should be a native conversion operation supported in Go (see #45320). I'm trying to create a Go constant that's a static function of some numbers and a bool. Having this be a function makes it impossible to use in crafting a Go constant.

The following would not be possible with this proposal:

const GPSEnabledMask = math.Bool(GPSEnabled) << 17

@jimmyfrasche
Copy link
Member Author

I would much rather this be a conversion but there's a long history of being against that so I filed this

@earthboundkid
Copy link
Contributor

As a practical matter, you can go in the opposite direction and define the GPSMask as a constant and then derive GPSEnabled as mask != 0. It does seem odd that you can only go in one direction.

@merykitty
Copy link

merykitty commented Aug 14, 2023

@carlmjohnson You can overload a mask with several meaning based on the constant information. E.g:

const AMask = int(EnableA)
const BMask = int(!EnableA)

Then if EnableA == true, set BMask will do nothing and get BMask will always return false.

@meyermarcel
Copy link
Contributor

The other idea is to have package bools, something like

func Int(bool) int // 0 or 1
func Compare(bool, bool) int
func Xor(bool, bool) bool
func Xnor(bool, bool) bool

Does this reflect Xor?

X != Y

and this reflect Xnor?

X == Y

If so I see no point to introduce syntactic sugar. This will harm readability because of inconsistent writing possibilities.

@earthboundkid
Copy link
Contributor

Xor is (a || b) && (a != b). Xnor is (a && b) || (!a && !b). They're annoying to read and write.

@meyermarcel
Copy link
Contributor

Xor is (a || b) && (a != b). Xnor is (a && b) || (!a && !b). They're annoying to read and write.

I agree that this annoying to write and read. Do I miss something here?

package main

import "fmt"

func main() {
	a1 := false
	a2 := true
	b1 := false
	b2 := true

	fmt.Println(a1 != b1)
	fmt.Println((a1 || b1) && (a1 != b1))
	fmt.Println()
	fmt.Println(a1 != b2)
	fmt.Println((a1 || b2) && (a1 != b2))
	fmt.Println()
	fmt.Println(a2 != b1)
	fmt.Println((a2 || b1) && (a2 != b1))
	fmt.Println()
	fmt.Println(a2 != b2)
	fmt.Println((a2 || b2) && (a2 != b2))
	fmt.Println()
	fmt.Println(a1 == b1)
	fmt.Println((a1 && b1) || (!a1 && !b1))
	fmt.Println()
	fmt.Println(a1 == b2)
	fmt.Println((a1 && b2) || (!a1 && !b2))
	fmt.Println()
	fmt.Println(a2 == b1)
	fmt.Println((a2 && b1) || (!a2 && !b1))
	fmt.Println()
	fmt.Println(a2 == b2)
	fmt.Println((a2 && b2) || (!a2 && !b2))
}

@merykitty
Copy link

x != y is the standard way to write boolean xor in a lot of programming languages

@earthboundkid
Copy link
Contributor

Yeah sorry, I was thinking about cases of non-booleans, where there are more than two values so you can't test it that way.

@rsc
Copy link
Contributor

rsc commented Dec 13, 2023

What I wrote in #61643 (comment) still applies: if you can gather evidence that this is an important conversion, then probably a direct language conversion is the answer. Otherwise we shouldn't do this either.

@jimmyfrasche
Copy link
Member Author

I created a little tool to help gather data: https://github.com/jimmyfrasche/issue61915

It's a command line programs that takes standard go tool patterns as arguments.

It searches for

  • function calls where the function is ~bool to a ~number
  • maps from a ~bool to a ~number
  • an if-else where each branch just sets a ~number to a literal or ident

It logs a file position for each hit to stderr then shows a summary for each package on stdout. For each package with hits, it shows counts for the implicit (if-else) and explicit (func/map) hits then their sum. If more than one package is selected it shows a total broken down the same way.

The if-else heuristic isn't perfect but it looks like it works fairly well in practice. It does overcount but the program itself undercounts as there are more esoteric ways to formulate things and there will be plenty of times the logic gets mixed in with more complicated situations

The explicit counters are far more exact, unless the function is operating on some hidden state like an RNG or closed over IO or something.

@rsc rsc changed the title proposal: math: a function to convert boolean expressions to numeric values proposal: math: add Bool(bool) int Dec 14, 2023
@rsc
Copy link
Contributor

rsc commented Dec 14, 2023

This proposal is a duplicate of a previously discussed proposal, as noted above,
and there is no significant new information to justify reopening the discussion.
The issue has therefore been declined as a duplicate.
— rsc for the proposal review group

@rsc rsc moved this from Incoming to Declined in Proposals Dec 14, 2023
@rsc rsc closed this as completed Dec 14, 2023
@gopherbot
Copy link
Contributor

Change https://go.dev/cl/550235 mentions this issue: go/analysis/passes/boolconv: analyzer to find int(bool) conversions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Declined
Development

No branches or pull requests

9 participants