Discussion Towards: Incremental Coding/ Native Query Language in Julia

WARNING: Visionary/ Feeling inspired. Call for advice, suggestions on how to proceed.

Summary

I imagine to be able to declare only the type of a variable and not assign it any value. This might be useful as It can act as typed hole, a placeholder of sorts, that still would allow some forms of type checking, code generation, pattern matching, meta programming, linting, syntax highlighting, IDE support etc.

TL;DR Given some type T, I want to be able to do x = @var(T) inside some limited context/ scope where it maybe useful to let it behave as if x were a term of type T.

EDIT: The above has been partially solved here. Opens discussions to other things below:

Intention for Useage

These are some ways I imagine wanting to use this:

  1. For incremental coding (inspired by Hazel):
@unfinished_code begin
x=@var(T) where Int<:T<:Real
y=x+2
end
  1. Using Julia natively as a query language:

I am aware that there are Julia ORMs. But to be able to express the constraints/ pattern of a query in Julia natively might open unexpected applications (as, database need not be the only target for such a query: for example Trustfall).

I am imagining something like these:

pattern = @search_pattern begin
s = @var(student)
1 <= s.studentID <= 5 || s.studentID = 5 || occursin(r'%Maximo', s.FullName)
s.sat_score > 1400 || s.sat_score < 1000
end

results = source.match(pattern)

instead of:

select studentID, FullName, sat_score, recordUpdated
from student
where
  (
    studentID between 1 and 5
    or studentID = 8
    or FullName like '%Maximo%'
  )
and sat_score NOT in (1000, 1400);

in sql, or maybe something like this:

 search_pattern = @pattern begin
 (r, tom, m) = @var(T, Person, Movie) where T<:relationship
 tom.name = 'Tom Hanks'
 r = T(tom,m)
 end

 results = source.match(search_pattern).collect(typeof(r), m.title)

instead of:

MATCH (tom:Person {name:'Tom Hanks'})-[r]->(m:Movie)
RETURN type(r) AS type, m.title AS movie

in cypher in Neo4j, or

  search_pattern = @pattern begin
      u, obj, act = @var(user, object, action)
      u.name='Kevin Morrison'
      obj.path='path'
      act.name = 'modify_file'
      permission(u,access(obj,act))
  end

  results = source.match(search_pattern).collect(obj.path)

instead of:

match
$user isa user, has full-name 'Kevin Morrison';
($user, $access) isa permission;
$obj isa object, has path $path;
$access($obj, $act) isa access;
$act isa action, has name 'modify_file';
fetch $path;

in typeql from typedb.

Conclusion

Yes, this is asking for a lot of syntactic sugar. And I don’t know if this is even possible. But I hope to open a discussion towards the right kind of syntactic sugar that is useful and possible.

Variables indeed can have declared types without assigned values. Access without a value errors, and so does assignment of values that can’t be converted to the declared type. Note that the local/global keywords are necessary because x::Int already means type assertion of the instance assigned to a variable.

julia> module TypedDemo
         global x::Int
         function foo(z)
           local y::Int
           y = z
         end
       end
Main.TypedDemo

julia> TypedDemo.x
ERROR: UndefVarError: `x` not defined in `Main.TypedDemo`
Suggestion: add an appropriate import or assignment. This global was declared but not assigned.
Stacktrace:
...

julia> TypedDemo.x = "one"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int64
...

julia> TypedDemo.foo("one")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int64
...

That alone doesn’t come close to the things discussed here, but maybe it can be useful.

@Benny Ah this was easier than I was thinking. Yes it solves my main question. But I will wait for discussion on the other things.

I have EDIT-ed the title of the post to reflect the focus of the question.

But, thank you so much!

So suppose I have a struct like:

struct Person
    name::String
    age::Int
end

Yes I can do global x::Person. But I can’t do x.name = "Harry" or global x.name = "Harry".

But maybe I could try creating the @var: struct --> struct macro that would create a new type that is mutable with the same attribute names but now with type Union(orginial_type, nothing) and initialize this type with nothing. This could allow rewriting with x.name = "Harry". Your thoughts?

Not for struct definitions alone because you’re only specifying the field structure there, too early to designate any particular values. Assigning values occurs in constructor methods that perform instantiation. However, @kwdef struct does support that syntax to be transformed into constructor method arguments with default values.

Thanks @Benny for @kwdef.

Here is my top-level idea:

  1. @var macro takes in a type T and creates an instance of type duck_T. This new type duck_T has almost the same structure as T except depending on some context it is initialized to nothing or any.

  2. There will be a macro called @allow_duck_typing. It creates a siloed environment inside which the types T and duck_T will be indistinguishable. I have not thought through fully how to implement this. But I am thinking to do duck_T <: __ducks__ where abstract type __ducks__ end. And then to do function over-ridding:

#feels controversial; what does it solve and _not_ solve?
Base.typeof(x::Duck_T) = T 

Question: Will traits be useful/ better here?

  1. Then @search_pattern sets context to any, as the end problem is a constraint satisfying problem (CSP). The actual lines of Julia inside this macro are irrelevant at run-time. They only have “meaning” at compile-time. And the “meaning” is to compile the code into an unified IR or AST capable of representing any CSP problem (like in SMT). Then depending on the type of the target where this CSP will be run against, the actual executable can be multiple-dispatched. The target can be different types of databases or even filesystems or APIs (like in Trustfall). This will be like the same code in Julia compiling against different hardwares (GPU/ CPU etc).

Porting the pattern to a SMT format can be useful: a SMT-solver maybe used to “simplify” the constraints and come up with an optimized query plan.

Question: maybe using a macro is an overkill. Looks like what I have described can be done with a function. I haven’t read enough from Julia to know what is better. Please advice.

  1. For the @unfinished_code, I would set the context to none, to make a more conservative guess. But I haven’t thought far.

  2. So intended use is something like this:

@allow_duck_typing begin

pattern = @search_pattern begin

# rest of code ...

end 

end