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

Symmetrically enforce the types of arguments to Boolean operators #9711

Closed
johnmyleswhite opened this issue Jan 10, 2015 · 17 comments
Closed
Labels
breaking This change will break code needs decision A decision on this change is needed won't change Indicates that work won't continue on an issue or pull request

Comments

@johnmyleswhite
Copy link
Member

Right now, we have a partial implementation of three-valued logic because the left-hand side of short-circuiting Boolean operators is checked for Bool typing at run-time, but the right-hand side is not checked for Bool typing if the operation short-circuits, since this would requiring evaluating the right-hand expression. This produces particularly odd results when you introduce Nullable{Bool}() into the mix, because you end up with things like:

julia> true || Nullable{Bool}()
true

julia> Nullable{Bool}() || true
ERROR: type: non-boolean (Nullable{Bool}) used in boolean context

julia> false && Nullable{Bool}()
false

julia> Nullable{Bool}() && false
ERROR: type: non-boolean (Nullable{Bool}) used in boolean context

I don't see any unambiguously correct solution to this given Julia's dynamic typing and the design objectives of Boolean operators in Base Julia, but there are some possible approaches:

  • Make || and && into a special evaluation rule that can be extended via dispatch. This has the undesirable consequence of allowing people to add truthiness to the language via possible extensions to Boolean operations.
  • Add special handling for Nullable arguments to || and && so that we can implement three-valued logic in full.
  • Verbally prohibit the implementation of three-valued logic, but note that the short-circuiting nature of || and && combined with run-time typing means that some cases will not raise errors despite being incorrect uses of || and && by convention.

Obviously, this all interacts with the Julia idiom of doing control-flow via Boolean operations.

@johnmyleswhite johnmyleswhite added needs decision A decision on this change is needed breaking This change will break code labels Jan 10, 2015
@JeffBezanson
Copy link
Member

I agree with your assessment that there is no good solution to this :)

@ivarne
Copy link
Member

ivarne commented Jan 10, 2015

Renaming && to and and || to or, would make them more obviously control flow operators, and avoid questions whether they should always return Bool.

@johnmyleswhite
Copy link
Member Author

I certainly agree that it is troublesome that we have | and || / & and &&, but one group consists of functions and the other consists of special operators without proper function semantics. That said, fixing that wouldn't solve the problem I'm concerned with here, which is that the possibility of executing true || Nullable{Bool}() without errors might fool you into believing you could successfully execute the expression Nullable{Bool}() || true. I don't see any viable solution given that people really like having Boolean operators short-circuit.

@ivarne
Copy link
Member

ivarne commented Jan 10, 2015

If && should no longer be short circuit, what would be the difference between && and &?

@johnmyleswhite
Copy link
Member Author

One would only be defined for Bool arguments.

@elextr
Copy link

elextr commented Jan 11, 2015

Removing the short circuiting nature of the && and || operators will make protecting expressions that are erroneous in some conditions much more verbose.

Programmers have long erroneously called them Boolean operators but, as you say, they are flow control and should be defined in that part of the manual.

Instead the & and | operators should be made Boolean rather than bitswise. Bitswise is likely to be much rarer (in user code at least) and so it seems a pity to waste good operator characters on it.

@JeffBezanson
Copy link
Member

I agree with that; I have wished several times that &, $, and | could be reclaimed for something else, and we could use bitand, bitxor andbitor instead. They are indeed quite rarely used.

@JeffBezanson
Copy link
Member

Although if we're going to reclaim those operators, merely having them be function versions of && and || also seems wasteful. Surely there are other functions more in need of infix syntax.

@srp
Copy link
Contributor

srp commented Jan 11, 2015

Given how often I've ended up in convert errors, part of me is a little
surprise that you didn't end up with an error:

ERROR: convert has no method matching convert(::Type{Bool},
::Nullable{Bool})

That does open the door to truthiness though, which is definately a mixed
bag.

On Sat, Jan 10, 2015 at 9:17 AM, John Myles White notifications@github.com
wrote:

Right now, we have a partial implementation of three-valued logic
http://en.wikipedia.org/wiki/Three-valued_logic because the left-hand
side of short-circuiting Boolean operators is checked for Bool typing at
run-time, but the right-hand side is not checked for Bool typing if the
operation short-circuits, since this would requiring evaluating the
right-hand expression. This produces particularly odd results when you
introduce Nullable{Bool}() into the mix, because you end up with things
like:

julia> true || Nullable{Bool}()true

julia> Nullable{Bool}() || true
ERROR: type: non-boolean (Nullable{Bool}) used in boolean context

julia> false && Nullable{Bool}()false

julia> Nullable{Bool}() && false
ERROR: type: non-boolean (Nullable{Bool}) used in boolean context

I don't see any unambiguously correct solution to this given Julia's
dynamic typing and the design objectives of Boolean operators in Base
Julia, but there are some possible approaches:

Make || and && into a special evaluation rule that can be extended via
dispatch. This has the undesirable consequence of allowing people to add
truthiness to the language via possible extensions to Boolean operations.

Add special handling for Nullable arguments to || and && so that we
can implement three-valued logic in full.

Verbally prohibit the implementation of three-valued logic, but note
that the short-circuiting nature of || and && combined with run-time
typing means that some cases will not raise errors despite being incorrect
uses of || and && by convention.

Obviously, this all interacts with the Julia idiom of doing control-flow
via Boolean operations.


Reply to this email directly or view it on GitHub
#9711.

@quinnj
Copy link
Member

quinnj commented Jan 12, 2015

If Nullable{Bool}() were handled specially to be allowed, such as Nullable{Bool}() && true, how would be control flow be handled if the value was null? Would the 2nd expression ever be evaluated?

Otherwise, it seems like the pattern to encourage would be

x = foo()::Nullable{Bool}
y = get(x,false) #or whatever default 2nd arg makes sense
y && continue_on_with_life()

I think that falls under your 3rd option? I'm not well-versed in the functionality/use of 3-valued logic, so perhaps that's why my initial reaction is to enforce get on the Nullable before using it in control flow.

@nalimilan
Copy link
Member

Maybe another, less disruptive solution: try to do type "light" inference on the right-hand side without evaluating. So Nullable{Bool}() would be detected as not being a Bool, thus preventing the most common error type, but e.g. false && continue_on_with_life() would not evaluate the function call. But maybe it's not possible to do type inference without evaluating?

Regarding the debate around operators, the previous reference is #5238

@johnmyleswhite
Copy link
Member Author

@quinnj: The main example where three-valued logic comes up is the evaluation of WHERE predicates in SQL (and their analogues in DataFrames indexing) where you want to include rows that match some predicates. There is also where things are most confusing about the current batch of Boolean special-forms + functions: you could safely do something like,

inds = Int[]
for i in 1:size(df, 1)
    if df[i, 1] | df[i, 2]
      push!(inds, i)
    end
end

but this will break with a change to if df[i, 1] || df[i, 2]. That's what I find too subtle.

It's exacerbated by the strong surface similarity of | and || that pushes in the opposite direction as the two having extremely different semantics.

@elextr
Copy link

elextr commented Jan 13, 2015

I used to be fond of the Ada syntax for && and || of and then and or else. The shortcutting was clearly obvious.

@srp
Copy link
Contributor

srp commented Jan 13, 2015

Erlang has and and or which are non-short-circuiting, and andalso and orelse which are short-circuiting: http://erlang.org/doc/reference_manual/expressions.html#id79610

@JeffBezanson
Copy link
Member

I don't think this is a bug. || and && have very simple behavior and I don't think adding subtleties will help.

@mbauman
Copy link
Member

mbauman commented Jan 13, 2015

Very related here is that the precedence of & and | is wonky when you go to use them as boolean-like operators. Much of what's being discussed here regarding the differences between && and & echoes the discussion in #5187.

@JeffBezanson
Copy link
Member

I'm sorry about this, but julia cannot give a type error about an expression without evaluating it, so this will have to stay as-is.

@JeffBezanson JeffBezanson added the won't change Indicates that work won't continue on an issue or pull request label Jan 27, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking This change will break code needs decision A decision on this change is needed won't change Indicates that work won't continue on an issue or pull request
Projects
None yet
Development

No branches or pull requests

8 participants