Syntax: How to write a cleaner code for many composed computations

Hi,

I have certain computations here. I use many parentheses just to make sure computations are all right. How can I write these Julia computations more cleaner?

p(x,y,α) = (-(2*α*y-1)^2)/(12*α^2) 

q(x,y,α) = ((2*α*y-1)^3-(27*α^2)*(x^2))/(108*α^3) 

del(x,y,α) = (27*α^2*x^4 - 2*(2*α*y-1)^3*x^2)/(1728*α^4) 


case1(x,y,α) = ((-(α*y+1)/(3α)) + cbrt((-0.5*q(x,y,α)) + sqrt(del(x,y,α))) + cbrt((-0.5*q(x,y,α)) - sqrt(del(x,y,α))))

theta(x,y,α) = (-q(x,y,α)/2)/((-p(x,y,α)/3)^(3/2))

case2(x,y,α) =  ((-(α*y+1)/(3α)) + ((abs(2α*y+1))/(3α))* cos((1/3)*acos(theta(x,y,α))))

r(x,y,α) = del(x,y,α) ≥ 0 ?  case1(x,y,α) : case2(x,y,α)

Example use:

α=1/2
(x,y) =(3,10)
xresult(x,y,α) = x/(1 + 2 * α * r(x,y,α));
yresult(x,y,α) = y + r(x,y,α);
(xresult(x,y,α),yresult(x,y,α))

Any references/comments are appreciated. It could be a silly question: but hoping to learn something new. Thank you for your time.

One approach is to use the let statement to organize intermediate results:

q(x,y,α) = let a=2α*y-1, b=3α*x, c=3α
    (a^3 - 3b^2) / 4c^3
end

although it seems like these intermediate expressions appear frequently … do they have names? If so, maybe it’s worth putting them into their own functions.

5 Likes

I have to say I’m not a fan of let blocks, which seem exotic and awkward to me (especially the way they appear above, with the confusing sequence of =s). And the above also appears to me to be a bit of an abuse of shortform function notation, which is really for one-liners.

I think a cleaner and more idiomatic solution would be

function q(x, y, α)
    a = 2α*y - 1
    b = 3α*x
    c = 3α
    return (a^3 - 3b^2) / 4c^3
end

In general, @manya, your code will also seem cleaner if you make judicious use of spacing around operators, in argument lists, etc.

14 Likes

Julia’s the first language that gives me let blocks and shortform function notation, and I just love putting them everywhere. Maybe I’m still in the honeymoon phase :sweat_smile:

I had originally written it as a one-liner (as is my default aspiration), but I wanted to space out the final expression a bit for some reason. Originally I had:

q(x,y,α) = let a=2α*y-1, b=3α*x, c=3α;  (a^3-3b^2)/4c^3  end
2 Likes

Yeah, I love short form too, but mostly for really short functions;)

I agree with you that it’s better to split some of these expressions into multiple ones. For example this

would be much more readable if split into sub-expressions. There are also repeated applications of some functions, which might be optimized away, but there’s no guarantee.

1 Like

Taking a second look, I think the OP’s needs are better served by putting everything in a single function:

function r(x, y, α)
    a, b, c = 2α*y-1, 3α*x, α*y+1

    q = (a^3-3b^2)/108α^3
    ∇ = x^2*(3b^2-2a^3)/1728α^4

    if ∇ ≥ 0
        rt∇ = √∇
        -c/3α + cbrt(-0.5q + rt∇) + cbrt(-0.5q - rt∇)
    else 
        p = -a^2/12α^2
        θ = -q/2(-p/3)^(3/2)
        -c/3α + abs(α*y+c)/3α*cos(1/3*acos(θ))
    end
end

This style breaks my (personal) convention of declaring locals with let or local, but it feels more legible this way. As long as the function definition is at global scope there’s nothing to worry about.

I’m not the happiest about having so many variables with seemingly meaningless names though. If there’s a factoring wherein the intermediate calculations can be given meaningful names, that’s definitely preferred.

And if there’s an academic paper which covers the derivation, it’s useful to put a link to it in the function’s docstring.

1 Like

I would tend to recommend using explicit return -c/3α + … in multiline functions like this, for clarity.

5 Likes

What’s the point of using , there instead of ;? Seems like the wrong notation

I’ve been avoiding return unless the function terminates early, but I always try different things in the hunt for style.

How about return if ∇ ≥ 0...? :wink: I like that it emphasizes that the return value will be found as the result of one of the following branches.

I think omitting return makes the code harder to read. I see a line in your if statement, and I think “huh, this code does nothing” until I realize that the if block is the last expression in your function body.

At one point early in Julia’s development @StefanKarpinski argued for making return statements mandatory except in short-form functions f(x) = ... or functions that return nothing, but at that point there was already too much inertia to change.

Explicit return statements are a common idiom in Julia code from what I’ve observed. See also this StackOverflow discussion.

14 Likes

Reading these threads, it’s fun to observe the strongly opposed opinions. :laughing:

It doesn’t seem to be a settled science, neither in those threads nor in the Julia codebase. From a quick hand-count of functions using longform named function syntax in array.jl (including macros and excluding things that obviously shouldn’t be counted like function f end), I count 25% that don’t have a return statement (23 out of 89), maybe half of which have a meaningful return value instead of relying on side-effects. So while it’s a minority position to omit return, and using return is certainly idiomatic, I don’t know that we can state unequivocally that omitting return is un-idiomatic. Maybe it’s the southern dialect of Julia, where we code with a lisp :wink:

I knew OO and procedural before anything else, so of course my default should be to use return. I’ll explain my reasoning for excluding it in Julia, and maybe you can tell me if my thinking is flawed.

My approach to this topic has not been from the perspective of a language architect choosing whether to favor functional or procedural styles, but as a user asking myself: given the rules of the language as they are, what is the optimal approach to consistently and concisely express my ideas while minimizing error rate? That question led me to the style of excluding return unless necessary.

The first attractor is the reduction in character count—that one’s obvious.

The second attractor is that using return less makes return more informational—as an indicator that something special is happening: an early function termination. Otherwise it should be assumed that the final value will be returned, because that’s exactly what will happen.

The third attractor is that it continually reminds me that the function will otherwise return the final value, which makes me more conscious of the function’s return value when writing functions with side-effects and (hopefully) reduces my error rate. Considering how high my intrinsic error rate is, any help is welcome :sweat_smile:

The fourth attractor is that it nudges me toward functional approaches, which I generally view as a good thing.

It’s been a very long time since I used Matlab, but I remember having the same internal debate and coming to the same conclusion back then too. Like end for closing code blocks, I started out with disliking it but eventually embraced it.

But I’m always open to the possibility that I’m wrong; I am quite often.

2 Likes

The comma-delimited assignments in the “head” of the let statement behave differently from assignments in its body.

The behavior of let statements is like function definitions, where the head is like the argument list for a function with default values:

a=1; d=0

let a=a, b=2, c=a+b
    d=a+b+c
    d^2
end     #-> 36

# is like
(  (a=a, b=2, c=a+b) -> begin
    d=a+b+c
    d^2
end)()  #-> 36

d == 0  #-> true

The variable scoping rules are the same.

The difference between assignments in the head and assignments in the body becomes starker for nested local scopes:

let a=1
    (let a=10; a end, a)
end   #-> (10, 1)
# versus
let a=1
    (let; a=10; a end, a)
end   #-> (10, 10)

note, again, that functions share these same behaviors; the key differences with let being the absence of a capture or boxing (and no extra compile time), and return behaves differently.

Interestingly, the example in the docs is erroneous (it’s probably showing an intended feature that’s not implemented yet).

1 Like

Looks like a typo; PR filed: fix typo in "let" docstring by stevengj · Pull Request #48042 · JuliaLang/julia · GitHub … Amazingly, this error was introduced in 2015 (basedocs #12438 part 4 by catawbasam · Pull Request #13217 · JuliaLang/julia · GitHub) and you seem to be the first person to have noticed it!

I take it back, it is apparently valid syntax, just nothing I’ve encountered myself:

julia> let x=4, y, z=5
          x+z
       end
9

julia> y = 3
3

julia> let x=4, y, z=5
          x+y+z
       end
ERROR: UndefVarError: y not defined
Stacktrace:
 [1] top-level scope
   @ REPL[3]:2

julia> let x=4, y, z=5
          y = x + z
          2y
       end
18

julia> y
3

So a variable without an assignment defines a new local variable in a let block but does not give it a value. I filed a PR to clarify this: clarify `let x` without `=` by stevengj · Pull Request #48043 · JuliaLang/julia · GitHub

2 Likes

My guess is that it was intended that an identifier without an assignment would define a new local variable in the let block and assign it to the value of the same identifier in the enclosing scope, à la function calls with keyword arguments and NamedTuple construction syntax.

Maybe this could be added to Julia 2.0, if it ever happens?

No. This feature goes back to extremely early versions of Julia, long before keyword arguments and named tuples.

1 Like

I doubt it. The time for purely aesthetic syntactic changes has passed and will never come again. PSA: Julia is not at that stage of development anymore

1 Like

Thanks so much for this discussion. I will upload a link very soon: that have fresh[young] analysis based many-many computations and new formulas which may be good to go in some existing Julia packages.