Fluent APIs in Julia


#1

I’m working on a SQL query builder module and I’d like to provide a fluent API using method chaining (https://en.wikipedia.org/wiki/Fluent_interface). My goal was something like:

select(...).from(...).where(...).limit(...).

After a bit of struggle because multiple dispatch, I ended up with what seems like a more Julian approach, overloading the + operator:

select(...) + from(...) + where(...) + limit(...)

Wondering if there’s a better way to implement method chaining in Julia?


However, when the API is used, it becomes verbose again as I have to prefix the methods with the module name (I don’t want to export the methods as the names are very common and would lead to name clashes – nor am I happy with const QB = QueryBuilder, still ugly). So I end up with:

QueryBuilder.select(...) + QueryBuilder.from(...) + QueryBuilder.where(...) + QueryBuilder.limit(...)

My solution is to provide a syntax similar to VisualBasic’s with blocks (https://docs.microsoft.com/en-us/dotnet/visual-basic/language-reference/statements/with-end-with-statement#example) - something like:

with QueryBuilder
  .select(...) + .from(...) + .where(...) + .limit(...)
end

Is it possible to write a similar @with macro in Julia? My first attempts failed because Julia complains when I try to build an expression which starts with a dot: ERROR: syntax: invalid identifier name ".". So I’m not sure how to attack this…

Thanks for your feedback, much appreciated!


#2

If you’re going to use a macro why not just use your original syntax!!?

@QueryBuilder select(...).from(...).where(...).limit(...)

Should be totally doable.


#3

I like Lazy's @> do ... syntax. But this is more than just chaining and going form x f g to g(f(x))?


#5

I feel => will be better than + as most of us will interpret + as some kind of addition while => is much more intuitive.
Eg: select(...) => from(...) => where(...) => limit(...) (Or should it be in opposite order??)
MXNet uses something similar to chain layers in neural networks (https://github.com/dmlc/MXNet.jl)


#6

One thing that I didn’t mention was that these methods can be chained in any order. This is one of the benefits of using a query builder versus just concatenating the parts of the actual query string. And this was also the reason for turning to fluent interfaces: so I wouldn’t have to manage a large number of combinations between different types (in my case, every method returns an instance of QueryPart).

@v-i-s-h in this regard I believe that + better reflects the commutative nature of the operation and the fact that the types are the same (one does not lead to the next).

@genauguy my issue has to do with scoping all the “dot” operations to an object – I can’t see a similar syntax with Lazy, can you give me an example, please?

@Raf That’s interesting - if it’s not possible to have an expression that starts with a . I could potentially “bootstrap” the expression. It can’t be select(...) as the methods can be chained in any order and they’re not required (select(...) can be absent, meaning SELECT *). But something like:
@with QueryBuilder.from(...).order(...).limit(...)
could be a good starting point.

Thanks


#7

I don’t think its possible to have.select

But it seems like you need to be very clear if you are making a macro-based DSL or using pure julia. Because the approach and syntax could be totally different. If your using a custom macro, you seem to be leaning to a DSL. So you may as well make it really clean syntax or its kind of a waste of using a macro.

So you don’t need the @with QueryBuilder part or the + or anything, just add them with the macro. You could swap the dots for spaces to use args in the macro, and you wont even have to parse much AST. Otherwise maybe don’t use a macro, its not worth the confusion and weirdness…

macro qb(args...)
       do_your_things(args)
end

@qb select(...) from(...) where(...) limit(...)

#8

That’s a good point. Looking at Query.jl, it defines a DSL using macros for each component. But I’d like to avoid exporting these definitions to avoid name clashes.

x = df |> @query(i, begin
            @where i.age>50
            @select {i.name, i.children}
          end) |> DataFrame

#9

why not use the pipe operator |> and make something similar to R’s dplyr/dbplyr with %>% replaced by |> ?


#10

That does seem like macro overkill, though there’s probably some reason…

Inside a macro those are just symbols :where etc and name space doesn’t matter, you can append or whatever to them if you have to keep them later in userspace (I have no idea what kind of transformation you actually have to do with them…)


#11

You could have a macro that converts from @qr select(...) where(...) from(...) to QR.select(...) + QR.where(...) + QR.from(...). This way if people don’t want to use the macro they can still use a convenient interface.


#12

Are there any idea to let julia support fluent interface? Ideally native support, without exposing macros to users.


#13

I’m quite happy with this implementation:

I defined a QueryPart type and multiple methods which return an instance of QueryPart. Then I defined a + operation on QueryParts.

It’s used like this:

select("username") + from(User) + where("id > 10") + order("joined_at") + limit(10)

Works quite nice and feels Julian. As an added bonus, the + operation is commutative so the queries can be composed in any order, as the parts are defined in code.


#14

I realized later, that we can overload a.b now. So maybe it is possible to use . instead of +?