Closed Moelf closed 10 months ago
PR Preview Action v1.4.4
:---:
:rocket: Deployed preview to https://JuliaHEP.github.io/JuliaHEP-2023/pr-preview/pr-15/
on branch gh-pages
at 2023-10-24 08:47 UTC
Some thoughts:
I think the contracted tables of operations are good - although I see you decided to keep "backwards division" in, despite it being a little subtle compared to the others. (I wouldn't object to removing it, thus resolving #13 )
I'm in two minds about removing the "typeclass" variable (restricted to ::Number) - I see your point, but it might be nice to have some example there that shows use of abstract types (maybe just an example of the supertype() or <: operators on some typed values?).
I'm okay with the change to broadcasting to use iseven(), since as you say it lets us demonstrate ? on a Julia library function to show how good it is, even if I miss the "even"/"odd" strings!
I think if we also want to resolve #12 we could easily remove the Compact Functions bit from the functions and control flow notebook, unless @graeme-a-stewart has a position on this?
I'm in two minds about removing the "typeclass" variable (restricted to ::Number) - I see your point, but it might be nice to have some example there that shows use of abstract types (maybe just an example of the supertype() or <: operators on some typed values?).
I think for practical audience you just need:
the whole \circ
thing is cute but not so useful IMO, can we put the anonymous function into the main notebook and delete the rest?
the whole
\circ
thing is cute but not so useful IMO, can we put the anonymous function into the main notebook and delete the rest?
By "the whole \circ thing", do you mean the entire discussion of function composition in Julia? If so, I'm not sure why you don't think it's useful - it's pretty significant that Julia will optimise composed functions as "entire units", especially since it means that it's much more efficient to broadcast (or map or whatever) over a composition. It especially motivates a Julia pattern of not avoiding "decomposing" complex functions into smaller reusable subfunctions, since composition will always allow the result to be optimised without additional function call overhead.
@graeme-a-stewart @tamasgal opinions in either direction?
function composition just means you can pass a function as an argument to another function. It doesn't surprise Python users.
\circ
is cute but can be confusing (another distraction), it has use cases but it's not a main thing they need to know about Julia, and doesn't come with any advantage in terms of performance or composability.
Likewise, I wouldn't mention >(3)
is functionally same as x -> x > 3
, even though I would use it day to day, but it's not important to know if you're just getting started
Honestly I'd much rather cover do()
syntax, like
map(ary) do x
...
end
# reminds Python users of their with
open("file.txt", "w") do f
println(f, "hello world")
end
cd(temp_dir) do
read(`ls`)
end
cuz it's used much more often than \circ
, and has implications in terms of capture or whatever.
So, \circ is different to just calling a function in a function (because "composed functions" are JITed as an entity - so once you do f \circ g once, the entity "f \circ g" is optimised and cached by the JIT, meaning that it's efficient to reuse it - compared to f(g(.)), which is not, and will be re-JITed each time). Indeed, that's the point of the @code_llvm example at the bottom of the function composition example - twice \circ thrice is demonstrably different in terms of the resulting llvm IR to twice(thrice()) - and yes, the native code produced looks different too. This probably will be surprising to python users.
(I do agree about do notation - I was considering making the "function composition" notebook a "cool function things" notebook with composition + do notation in it, but this was the minimum viable product)
So, \circ is different to just calling a function in a function (because "composed functions" are JITed as an entity - so once you do f \circ g once, the entity "f \circ g" is optimised and cached by the JIT, meaning that it's efficient to reuse it - compared to f(g(.)), which is not, and will be re-JITed each time).
I guess I don't know what do you mean here -- Julia doesn't "re-jet", in fact, when you call f()
inside g()
, f()
is often compiled before g()
even starts:
julia> @generated function g(x)
println("this is compile time for g($x)")
return zero(x)
end
g (generic function with 1 method)
julia> @generated function f(x)
println("this is compile time for f($x)")
return one(x)
end
f (generic function with 1 method)
julia> function main(x)
@info 1
z = f(x)
@info 2
g(z)
end
main (generic function with 1 method)
julia> main(3)
this is compile time for f(Int64)
this is compile time for g(Int64)
[ Info: 1
[ Info: 2
0
you might be saying something technically true, but I don't think saying one JIT more the other JIT less is accurate.
The LLVM/Native code difference doesn't mean \circ
is better, in fact, it's probably doing extra stuff to pull functions out of the ComposedFunction{...}
struct, which definitely doesn't make things faster.
If you look at the ComposedFunction
call implementation:
https://github.com/JuliaLang/julia/blob/0be0b389e300c05abde796f68abbdd95b938f7ad/base/operators.jl#L1041-L1045
it just recursively call each element of the Tuple{...}
functions, and you can easily have type inference failure, which has been spotted in the past:
Hm, it seems you're right - despite the fact that @code_llvm and @code_native insist that the generated code is different (and, notably, elides the internal function call in the \circ composed version, producing more optimised code...), the performance characteristics seem to be basically identical for a number of test cases I just played with
[Which might mean that @code_native and @code_llvm are not quite telling the truth? I'd expect a function call to be noticeable!]
That said, it's still faster than generating a new anonymous function for use in a map or something (that is: map( f \circ g, array) is faster than map( x-> f(g(x), array), especially after the first time, because Julia does compile a new anonymous function each time here, but just keeps around the composed function object once you use it once). Not sure if that's worth the notebook though?
yeah I would maybe call those "performance gotcha", not super important and likely to be case-by-case I think?
Aha, I understand what's happening with @code_foo - it's the curse of the JIT liking to optimise things inside function bodies. (That is: h(x) = f(g(x) and h(x) = (f \circ g)(x) produce identical @code_llvm output, so the problem is that the introspection is "lying" about function composition that's "bare" because it doesn't reflect what actually happens on invocation.)
Anyway, my suggestion is that we either:
completely remove extra "function composition" notebook (and rewrite the mandelbrot set broadcasting example to use a chain of broadcast function calls - which also leads into the "broadcasting is efficient" discussion after it)
or
replace it with a notebook discussing do() notation (and also probably rewrite the mandelbrot set example as above)
ready for review @aoanla
I replaced the notebook content (and name in _toc) with anonymous function and do-block
, also fixed the Mandelbrot to use broadcast and took the chance to tell audience about broadcast fusion.
Hi - thanks for all of these improvements! Covering do-block
is a great thing as it's very Julian - it confused me the first time I saw it until I found out what was going on.
fix #13, #14