queryverse / queryverse/Query.jl

Use higher order approach to null lifting

Open
#71 3 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

wontfix
Dominant language
Julia
Stars
403
Forks
48
Avg merge
3d 6h
Merged PRs (30d)
6

Description

This is a straw-man issue to put down my thoughts why this is not a good approach. There are two parts. The first is a discussion of some technical aspects. Those could in principle be overcome by e.g. new language features. The second is a discussion of the semantics of lifting, and why I think a general approach is futile in that area.

Technical aspects

  1. It looks like a design smell to me to try to solve the problem of operations on Nullables in a query framework. Those two issues are orthogonal. Expressions with Nullables occur outside of queries as well, and any solution that only works within a query framework would not help with the situation outside of a query framework.
  2. I'm a strong believer in the following principle: an expression that operates on some inputs should always have the same semantics, regardless where that expression occurs. The surroundings of an expression should, as much as possible, not change the semantics of the expression. So for example, I'm adamantly opposed that the expression in the @where clause here:
@from i in source begin
    @where i + 4
    @select i
end

has a different semantics than what say Nullable(2) + 4 would have in some non-query context. I think that kind of context-dependent semantics changes are highly confusing for users and generally a sign of a bad system design, because one can't reason about the semantics of the system based on first principles anymore, instead one needs to learn a list of rules how things work in different contexts.
3. I think of queries as very similar to generator expressions. In fact, I want simple query expression that only project and filter to be isomorphic to the equivalent generator expression. For example, I want to make sure that

@from i in source begin
    @where i>2
    @select i^2
    @collect
end

behaves exactly in the same way as

[i^2 for i in source if i>2]
  1. I think one should always be able to factor out an expression from a query into a function and still get the same result. For example, lets say one starts with this query:
@from i in source begin
    @select i^2
end

Then I want the following code to behave exactly in the same way:

foo(x)=x^2
@from i in source begin
    @select foo(i)
end
  1. I want the following code to work as one would expect it to work:
foo(x)=isnull(x) ? 0 : x^2
@from i in source begin
    @select foo(i)
end

The higher order lifting approach in queries violates all of these things, and I think that is too high of a cost.

Semantics of lifting

The current idea for any general lifting semantics story is the lifted version of a function would return a null value if any of its inputs are null. I think that makes sense for some functions, but so far I can think of at least two groups of functions where this is actually not the correct semantics. This makes the situation very different from the vectorization story, where it is clear that the application of the . syntax should have the same semantics in all cases. Here are the two cases that in my mind should not follow a general lifting semantics:

  1. Any predicat. These include things like comparison operators (==), but also functions like contains etc. This is a large group of functions. I think predicate functions that take a Nullable as an input in almost all cases should return a Bool and not a Nullable{Bool}. Why? Because otherwise one will constantly end up dealing with 3VL, which is unintuitive and a pain and plain and simply confusing.
  2. The operators of 3VL. Now this is slightly ironic, because my previous point of course argued that we should try to keep 3VL out of the system as much as possible, but still. If you do have a Nullable{Bool}, then & and | should follow the 3VL rules, which are different from any generic lifting strategy.

I think these two examples are actually enough to conclude that a generic lifting strategy here is not a good idea. With this list we already have at least three types of functions that should all get different semantics, and who knows, we might well stumble upon another group of functions with yet another natural lifting story.

The case for a white-list lifting approach

Various folks have suggested that a white-listing approach to lifting is bound to not work. I disagree. So far I've seen two arguments against it: 1) it is too much work to define lifted versions (variants of that are "how do we decide what to lift?", "these are too many methods", "we'll never cover all functions") and 2) user-defined functions can't use multi-dispatch if they want to work with a set of minimal lifted functions.

I think 1) is not a convincing argument at all. That argument can be made about pretty much anything. We'll never be able to implement all math functions in julia. Sure, but why should that stop us implementing as many as we can? I think the right response to this problem is to roll up our sleeves and write code. We might not get to a 100% coverage, but I'm sure we could get to a coverage level that is good enough for the vast number of data science use cases. And for the remaining cases there is a very simple solution for users. Just write isnull(x) ? bla : foo(get(x)).

Number 2) is more difficult, but I think in practice also not that bad. There are two types of user-defined functions in my mind: the small anonymous functions that get written in frameworks like Query.jl and StructuredQuries.jl as e.g. the filter condition etc. The arguments of those are by default not typed, so I think things should work just fine there. The second type of user-defined function is one where the user defines the method properly and wants to dispatch on types. There is no good solution for that with the white-list approach, i.e. if the user wants his/her function to work with Nullables, he/she will have to define a lifted method. Not ideal, but I still prefer that over all the inconsistencies that are introduced by the higher-order lifting approach. There might also be some clever way to get around this using traits.

Summary

Long story short, in my mind the drawbacks of a higher-order lifting approach are 1) technical that they violate some basic principles that I think are key for a good language design and 2) I don't think there is a general lifting semantics that one can slap on all functions. The latter point seems to biggest difference to the vectorization story.

At the end of the day all of that in my mind suggests that for now we should stick with a white-list approach. If someone comes up with a convincing general lifting strategy (both from a technical and from a semantic point of view) we can revisit that decision.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

The issue is a design discussion and names no files, entry points, or tests. Start by reading the technical and semantic arguments about higher-order versus white-list lifting, then inspect the current null-lifting implementation in Query.jl. Done would require a decided lifting strategy that addresses the stated semantic and dispatch concerns.

Written by the indexing model from the issue text.

Assessment

Tech stack
julia
Domain
data
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
15/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.