Hi Sariel, welcome to the community. I used to struggle with accepting the code like x && println("OK") too (where x is either true or false). But the section in the manual explains it nicely, I guess. I am now at peace with the notation. In fact, it then also makes perfect sense to write x || println("OK") instead of !x && println("OK").
I mostly avoid && and || in package code, because it decreases the granularity of code coverage. And of error message location info in general. Related issue:
You could always write a macro to replace ⟹ by &&. E.g.
julia> macro ⟹(expr)
replace_implies_call_with_short_circuiting_and!(expr)
return expr
end;
julia> function replace_implies_call_with_short_circuiting_and!(expr)
if expr.head === :call && expr.args[1] === :⟹
expr.head = :&&
popfirst!(expr.args)
for a in expr.args
a isa Expr && replace_implies_call_with_short_circuiting_and!(a)
end
end
end;
julia> @⟹ true ⟹ println("Will print");
Will print
julia> @⟹ false ⟹ println("Won't print");
Logically x ⟹ y is !x || y and could thus be used to the desired effect. Unfortunately, short-circuiting requires delayed evaluation which requires macros in Julia and infix macros cannot be defined. A combination of ⟹ and a macro could be used:
I thought that Match.jl might be what you want, but then I realized it might be an overkill for your situation, where you’re most likely trying to make a one-liner code.
Although you can make an one-liner code using Match.jl, it will error if the condition is not met since you can’t exhaust all the possible conditions, and that won’t be something you’d want.
It does feel like a hack because && is primarily a boolean AND. This notation => makes more sense for a conditional, but I’d rather not add more syntax when the typical if (condition) do_something end already works and makes as much sense. Then again, I am biased because I prefer if end one-liners over && to begin with.
It’s also worth pointing out that short-circuiting semantics are not strict like other arithmetic operators (which act only after all inputs are evaluated), so they are formally control flow as well, which is why they are used as such in many languages for over half a century. A few dynamically typed languages, like Julia, lean into it by conditionally returning the 2nd expression, which may not be boolean. This is also a reason I personally prefer if end; on the occasion I assign the value of a conditional statement, I intend a potentially stable type from the branches or nothing, not a particular value of the condition depending on the operator.
I have an feature request (that would be breaking, and shouldn’t be seriously considered for a very long time) for backwards-looking macros. This seems like a scenario where they might be useful.
I don’t like the lack of mandatory end or some other block delimiter. It’d be fine if it were restricted to a binary operation, but the conversation naturally involved else (which has historically been the case), and that runs into the long-solved dangling else problem:
if a then if b then x else y
could be
if a then (if b then x) else y # y depends on a
or
if a then (if b then x else y) # y depends on b
Julia’s end-less syntax must actually disambiguate that example with || vs ?: for if-then vs if-then-else respectively, but I go through a similar struggle for:
a, b, x, y = true, false, "hello", "world"
a ? b : z ? x : y
#=
Is it
(a ? b : z) ? x : y
or
a ? b : (z ? x : y)
=#
I struggle with the precedence of binary operators as is, I can’t see it’s the latter, whether intuitively or with the operator precedence table on hand. Granted, the equivalent if a b elseif z x else y end statement is hard to read, but at least I can tell where the branches are.
PS you can fake then right now with if a #=then=# b end. If that seems a tad long, there’s the even shorter if (a) b end or if a; b end to explicitly divide the condition and branch.