Can I use a closure, and if yes how?

I have the following code:

using ControlPlots
using KiteModels, KitePodModels, KiteUtils

set = deepcopy(se())
kcu::KCU = KCU(set)
kps4::KPS4 = KPS4(kcu)

include("../examples/plot2d.jl")

reltime = 0.0
integrator = KiteModels.init_sim!(kps4, stiffness_factor=0.5)

lines, sc, txt = nothing, nothing, nothing
lines, sc, txt  = plot2d(kps4.pos, reltime; zoom=true, front=false, segments=set.segments, lines, sc, txt)
nothing

The included file looks like this:

function plot2d(pos, reltime=0.0; zoom=true, front=false, segments=6, fig="", lines, sc, txt)
    x = Float64[] 
    z = Float64[]
    for i in eachindex(pos)
        if front
            push!(x, pos[i][2])
        else
            push!(x, pos[i][1])
        end
        push!(z, pos[i][3])
    end
    x_max = maximum(x)
    z_max = maximum(z)
    xlabel = "x [m]"
    if front xlabel = "y [m]" end
    if isnothing(lines)
        if fig != ""
            plt.figure(fig)
        end
        lines=[]
        line, = plt.plot(x,z; linewidth="1")
        push!(lines, line)
        sc  = plt.scatter(x, z; s=25, color="red") 
        if zoom
            txt = plt.annotate("t=$(round(reltime,digits=1)) s",  
                xy=(x_max, z_max+4.7), fontsize = 14)
            plt.xlim(x_max-15.0, x_max+20)
            plt.ylim(z_max-15.0, z_max+8)
        else
            txt = plt.annotate("t=$(round(reltime,digits=1)) s",  
            xy=(x_max, z_max+8.0), fontsize = 14)
            plt.xlim(0, x_max+20)
            plt.ylim(0, z_max+20)
        end
        if length(pos) > segments+1
            s=segments
            line, = plt.plot([x[s+1],x[s+4]],[z[s+1],z[s+4]], linewidth="1"); push!(lines, line) # S6
            line, = plt.plot([x[s+2],x[s+5]],[z[s+2],z[s+5]], linewidth="1"); push!(lines, line) # S8
            line, = plt.plot([x[s+3],x[s+5]],[z[s+3],z[s+5]], linewidth="1"); push!(lines, line) # S7
            line, = plt.plot([x[s+2],x[s+4]],[z[s+2],z[s+4]], linewidth="1"); push!(lines, line) # S2
            line, = plt.plot([x[s+1],x[s+5]],[z[s+1],z[s+5]], linewidth="1"); push!(lines, line) # S5
        end
        plt.xlabel(xlabel, fontsize=14)
        plt.ylabel("z [m]", fontsize=14)
        plt.grid(true)
        plt.grid(which="major", color="#DDDDDD")
    else
        lines[1].set_xdata(x)
        lines[1].set_ydata(z)
        if length(pos) > segments+1
            s=segments
            lines[2].set_xdata([x[s+1],x[s+4]]) # S6
            lines[2].set_ydata([z[s+1],z[s+4]]) # S6
            lines[3].set_xdata([x[s+2],x[s+5]]) # S8
            lines[3].set_ydata([z[s+2],z[s+5]]) # S8
            lines[4].set_xdata([x[s+3],x[s+5]]) # S7
            lines[4].set_ydata([z[s+3],z[s+5]]) # S7
            lines[5].set_xdata([x[s+2],x[s+4]]) # S2
            lines[5].set_ydata([z[s+2],z[s+4]]) # S2
            lines[6].set_xdata([x[s+1],x[s+5]]) # S5
            lines[6].set_ydata([z[s+1],z[s+5]]) # S5
        end
        sc.set_offsets(hcat(x,z))
        txt.set_text("t=$(round(reltime,digits=1)) s")
        plt.gcf().canvas.draw()
    end
    sleep(0.01)
    lines, sc, txt
end

When the function plot2d is called the first time, a plot is created. Any further call results in just moving the line and updating the text, but not creating anything new.

In the end we get a 2D movie…

Now my question:
I create the three variables

lines, sc, txt = nothing, nothing, nothing 

and also return them from the function to keep the state…
Normally the function is called in a loop, see for example: KiteModels.jl/examples/reel_out_4p.jl at e3142cc7e3a6473f23a465f4d71cc80e3eebc4ed · ufechner7/KiteModels.jl · GitHub

Is there a way to create a closure such that I do not have to pass or return these variables?

1 Like

For the code you linked, something like this should work:

# Instead of lines, sc, txt = nothing, nothing, nothing
# define a closure over that state
plotter = let lines = nothing, sc = nothing, txt = nothing  # Note: Must all be on same line as let!
              function(x, y; kwargs...)
                  lines, sc, txt = plot2d(x, y; lines, sc, txt, kwargs...)
              end
          end
# ...
# Now in the loop call plotter instead of plot2d
if mod(i, 5) == 0
   plotter(kps4.pos, reltime; zoom=ZOOM, front=FRONT_VIEW, 
                              segments=set.segments, fig="side_view")            
end

This “let-over-lambda” construction is a general idiom for creating stateful closures and broadly applicable, i.e., I did not check what plot2d actually does.

2 Likes

This actually works, thanks a lot!

function plot2d_(pos, reltime; zoom=true, front=false, segments=6, fig="", lines, sc, txt)
    x = Float64[] 
    z = Float64[]
    for i in eachindex(pos)
        if front
            push!(x, pos[i][2])
        else
            push!(x, pos[i][1])
        end
        push!(z, pos[i][3])
    end
    x_max = maximum(x)
    z_max = maximum(z)
    xlabel = "x [m]"
    if front xlabel = "y [m]" end
    if isnothing(lines)
        if fig != ""
            plt.figure(fig)
        end
        lines=[]
        line, = plt.plot(x,z; linewidth="1")
        push!(lines, line)
        sc  = plt.scatter(x, z; s=25, color="red") 
        if zoom
            txt = plt.annotate("t=$(round(reltime,digits=1)) s",  
                xy=(x_max, z_max+4.7), fontsize = 14)
            plt.xlim(x_max-15.0, x_max+20)
            plt.ylim(z_max-15.0, z_max+8)
        else
            txt = plt.annotate("t=$(round(reltime,digits=1)) s",  
            xy=(x_max, z_max+8.0), fontsize = 14)
            plt.xlim(0, x_max+20)
            plt.ylim(0, z_max+20)
        end
        if length(pos) > segments+1
            s=segments
            line, = plt.plot([x[s+1],x[s+4]],[z[s+1],z[s+4]], linewidth="1"); push!(lines, line) # S6
            line, = plt.plot([x[s+2],x[s+5]],[z[s+2],z[s+5]], linewidth="1"); push!(lines, line) # S8
            line, = plt.plot([x[s+3],x[s+5]],[z[s+3],z[s+5]], linewidth="1"); push!(lines, line) # S7
            line, = plt.plot([x[s+2],x[s+4]],[z[s+2],z[s+4]], linewidth="1"); push!(lines, line) # S2
            line, = plt.plot([x[s+1],x[s+5]],[z[s+1],z[s+5]], linewidth="1"); push!(lines, line) # S5
        end
        plt.xlabel(xlabel, fontsize=14)
        plt.ylabel("z [m]", fontsize=14)
        plt.grid(true)
        plt.grid(which="major", color="#DDDDDD")
    else
        lines[1].set_xdata(x)
        lines[1].set_ydata(z)
        if length(pos) > segments+1
            s=segments
            lines[2].set_xdata([x[s+1],x[s+4]]) # S6
            lines[2].set_ydata([z[s+1],z[s+4]]) # S6
            lines[3].set_xdata([x[s+2],x[s+5]]) # S8
            lines[3].set_ydata([z[s+2],z[s+5]]) # S8
            lines[4].set_xdata([x[s+3],x[s+5]]) # S7
            lines[4].set_ydata([z[s+3],z[s+5]]) # S7
            lines[5].set_xdata([x[s+2],x[s+4]]) # S2
            lines[5].set_ydata([z[s+2],z[s+4]]) # S2
            lines[6].set_xdata([x[s+1],x[s+5]]) # S5
            lines[6].set_ydata([z[s+1],z[s+5]]) # S5
        end
        sc.set_offsets(hcat(x,z))
        txt.set_text("t=$(round(reltime,digits=1)) s")
        plt.gcf().canvas.draw()
    end
    sleep(0.01)
    lines, sc, txt
end

plot2d = let lines = nothing, sc = nothing, txt = nothing  # Note: Must all be on same line as let!
    function(pos, reltime=0.0; kwargs...)
        lines, sc, txt = plot2d_(pos, reltime; lines, sc, txt, kwargs...)
    end
end

and I call it with:

    if mod(i, 5) == 0
        plot2d(kps3.pos, reltime; zoom=ZOOM, front=FRONT_VIEW, 
                       segments=set.segments, fig="side_view")             
    end

This is nice, because now I can move this code into a package, and if I should need more variables for defining the state I do not have to change the code that is using this package…

Thanks a lot!

What does lambda mean in this context?

Anonymous functions are called/created with lambda in many functional languages (including Common Lisp where I had first encountered this idiom):

let cnt = 0
     x -> cnt += x
end

would be

(let ((cnt 0))
   (lambda (x) (incf cnt x)))

in Common Lisp

More generally it’s a reference to “lambda calculus”.

1 Like