With struct do

A long time ago, I learned Turbo Pascal and there was a contract that allowed:

with x do 
     field3 = field1 + field2    
end

instead of

x.field3 = x.field1 + x.field2

where

mutable struct compositetype
     field1
     field2
     field3
end

x = compositetype(1,2);

Is that possible in Julia?

You could write a macro to do something a bit like this, although it hits a bunch of problems with figuring out which symbols in the expression could be fields in the struct: a macro can’t know that information, so you’d need purely syntactic rules to determine whether something’s a field. In particular, how do you handle the case in which the struct contains a function in it?

with x do
    field3 = field4(field1, exp(field2))
end

There is not currently anything like this, I think. @unpack from Unpack.jl gives you something similar.

DataFramesMeta and other Meta packages will also let you do this in some way, if you need help figuring out how to write a macro for this.

2 Likes

I think PatameterisedModule.jl by @thautwarm may do what you want. It may be a bit too fancy for your uses though. I certainly don’t really ‘get’ it as I never used Ocaml.


Edit: Nevermind, it appears to only support immutable types if I understand correctly.

Julia also has functors.

Could you expand on how that would help the OP do what they ask?

I’m giving the user more options, so they can think about how they want to write their program. The functor looks pretty close to the x = compositetype(1,2) line in the OP (and so does an outer constructor!).

I came up with something here: Unpack named tuples (does anyone know how to do it?) - #13 by Mason but I don’t think there’s any good way to turn a setfield! statement like the one you show: field3 = field1 + field2 into x.field3 = x.field1 + x.field2 so I posted it in the other thread rather than this one.

Not directly. As others have suggested, various approximations exist, but fundamentally this kind of a construct does not fit well into the way Julia works.

In a nutshell, the meaning of field3 etc in a with block would need to depend on the type of x, which may or may not be known at compile time. Consider

function f!(x)
    with x do # **** NOTE: hypothetical syntax ****
        field3 = field1 + field2    
    end
    x
end

mutable struct Foo
    field1
    field2
end

mutable struct Bar
    field1
    field2
    field3
end

f!(Foo(1, 2))
f!(Bar(1, 2, 4))

What should f!(Foo(1, 2)) do? Should it treat field3 as a local variable, which is then optimized out as unused? This could lead to very insidious bugs.

5 Likes

OP, another solution is Lazy.jl’s @as functionality. If the variable you are working with has a long name, you can shorten it in a block using @as.

julia> my_very_long_nt = (a = 1, b = 2)
(a = 1, b = 2)

julia> @as t my_very_long_nt begin
       t.a + t.b
       end
3
2 Likes

If the threaded arguments from @as are not needed, why not a let block:

let t = my_very_long_nt
    t.a + t.b
end
1 Like

If you’re willing to explicitly mark which symbols should be treated as fields, then this is easier to implement. Here’s a macro which will replace all expressions of the form &a with container.a:

julia> using MacroTools: postwalk

julia> macro with(container, expr)
         esc(postwalk(expr) do ex
           if @capture(ex, &field_)
             :(($container).$field)
           else
             ex
           end
         end)
       end
@with (macro with 1 method)

Usage:

julia> mutable struct Bar
           field1
           field2
           field3
       end

julia> b = Bar(1, 2, 0)
Bar(1, 2, 0)

julia> @with b begin
         &field3 = &field1 + &field2
       end
3

julia> b
Bar(1, 2, 3)

This implementation doesn’t have any problem with local variables which happen to match the names of fields, since the user is responsible (by adding &) for explicitly marking which symbols should be treated as fields:

julia> field1 = π
π = 3.1415926535897...

julia> @with b begin
         # The left-hand side is unambiguously `b.field1` 
         # and the right-hand side is unambiguously
         # the local variable `field1`.
         &field1 = field1
       end
π = 3.1415926535897...

julia> b
Bar(π, 2, 3)
6 Likes

They’re not the same “functor”.

functor in C++ is just a callable, that’s quite a simple thing and not suggestive enough to abstraction tasks in programming.

The category theory has a concept called functor, and specifically, something called endofunctor will always take the place of a general “functor”, in programming fields, because usually only “endofunctor” is useful for programming.

What I meant by functor is close to the second meaning mentioned above:
When we have a module A(like a category), and a functor F that maps it to another module F(A), where some types (like A.t) in module A will be mechanically mapped to F(A.t), as well as values and functions.

A function f defined in the module A, transforms a value typed A.t1 to a value typed A.t2, it will be mapped correspondingly into module F(A), as a function F(f).

F(f) transforms a value typed F(A.t1) to a value typed F(A.t2).

original mapped
module A F(A)
type t F(t)
a function’s “effect” from t1 to t2 from F(t1) to F(t2)

See Catlab.jl, they’re professional and they should have shown use cases of this functor.

This one is already available… with GeneralizedGenerated.jl, but I hope users do not write code like this…

Locally and implicitly binding variables from data structures is not a new thing, in some other languages they widely use this.

with! treats the fields as-is(visiting these variables means accessing fields), with unpacks and binds the fields to local variables. This makes sense to me.

Is there a good tutorial on how to do this? In particular, is there a way to evaluate field1 instead of using its variable name as a literal? i.e. x = :field1 and &x still get field1?

No, &x unambiguously means container.x not container.(whatever the value of the variable `x` currently is). This is a purely syntactic transformation, so the values of variables cannot affect the code produced by the macro.

If you want to set a struct field using a name contained in a variable, then you need setproperty! instead.

I am aware of this (eg R does this — it is even called with), I just don’t think it is a good thing as it can lead to very messy code.

1 Like

Thanks! One other question, sorry to bother you. Do you know how to @capture a symbol literal? Like, not any particular symbol literal, but do something if an expression is just a symbol literal.

The easiest way is just:

if x isa Symbol
  ...
end

If you prefer to work in @capture syntax, you can use the _Symbol suffix:

julia> @capture(:hello, x_Symbol)
true

julia> x
:hello

see: https://mikeinnes.github.io/MacroTools.jl/stable/pattern-matching/#Matching-on-expression-type-1

Hmmm still doesn’t work. (I’m playing around with implementing some DataFramesMeta features to try and learn about it).

If the mods want to break this into a new thread I would appreciate it.

using DataFrames
using MacroTools
using MacroTools: postwalk

macro with(container, expr)
  esc(postwalk(expr) do ex
    if @capture(ex, &field_)
      quote 
        $field isa Union{<:Integer, <:AbstractString, Symbol} ? 
          getindex($container, !, $field) :
          Tables.columntable(getindex($container, !, $field))
      end
      elseif @capture(ex, x_Symbol)   
        :(getindex($container, !, $x))
      else
        ex
      end
  end)
end

df = DataFrame(rand(2,2))
@with df begin
    :x1
end