CHAPTER 18

Programming in Scripting Style

In the first chapter, we have promised to help the programmers to complete a full language transition from compiled to interpreted. With what we have learned so far, it's time to revisit our programming pattern and finally adapt to the faster coding practice in a scripting environment. This chapter highlights some of the main techniques you will be using most frequently when casually writing Julian:

Use only builtin types

Forget about defining classes. You will only need the following types:

  • Primitive types (int, bool, byte, float and char)
  • String
  • Function
  • Dynamic
  • Untyped (var)
  • Array of above (but remember var[] is not covariant)

That's about enough to get us anywhere. In particular, always combine the use of var and Dynamic: when you need to declare variable, parameter and return type for which you would have to define a class in compiled language, simply use Dynamic.

  var machine = new { // Create a dynamic with minimal syntax
    size = 17,
    name = "hammer",
    operate = () => { ... }
  };

  var processWith(var tool){ // Use var instead of Object types
    string name = tool.name; // Use the known built-in type where possible
    int size = tool.size;
   
    Console.println("Validating " + name);
    validateSize(size);
   
    string name = tool.name;
    Console.println("Processing with " + name);
   
    return tool.operate();
  }

Use Dynamic object without customization

Customizing Dynamic involves defining new class, which is something we want to avoid. In fact, most of times you will find that the default behavior of Dynamic is quite sufficient already. Since it will automatically bind to a lambda literal, simply wrap your function in a lambda and pass the value along.

  var process(var tool) { // Do not write function that requires explicit binding to work. Instead, pass along all the arguments.
    return ...;
  }

  var machine = new {
    size = 17,
    name = "hammer",
    operate = () => {
      return process(this); // 'this' will be bound to machine
    }
  };

If you do not want to expose everything from a dynamic, you can use a lambda to enforce the separation of scopes. This is a technique most commonly associated with JavaScript's closure.

  var machine = (() => {
    int _size = 17;
    return new {
      name = "hammer",
      getSize = () => {
        return _size; // _size is in the scope.
      }
    };
  })(); // Evaluate immediately to obtain the Dynamic object

After the dynamic is created, there is no way to modify the value for _value, since it's not a property on the dynamic.

Use default module path and simplified module declaration

Julian supports customized script module path. Do not use it unless you have to. Instead, always go with the default module path, jse_modules. For more details, see here.

When you start a new Julian project, create a folder with name jse_modules alongside your main script. If you even need to define a module, put it there. The bonus of such practice is that you don't need to declare the module name in the module files. Simply put a module; statement at the beginning of the file and start defining types.

\Path
    +- main.jul
    +- \jse_modules
                  +- \A
                  |   +- \B
                  |       +- \C
                  |           +- file3.jul
                  +- \X
                      +- \Y
                          +- file4.jul

  // main.jul
  import A.B.C;
  
  Item item = new Item(); // Refer to the type defined in module A.B.C
  item.work();
  ...
  
  // file3.jul
  module; // That's it. No need to repeat yourself.
  
  import X.Y;
  
  class Item {
   Entity entity; // Refer to another module
  }
  
  // file4.jul
  module;
  
  class Entity { }
  enum EntityType { }

The module search paths are fixed at the start of engine session, so not only the main script, but every module script will be able to find other modules as they will all look at a single module path which is jse_modules located alongside the main script.

Share functions by script inclusion

Another means for code sharing in Julian is through script inclusion. While we don't usually recommend this alternative, we do acknowledge its usefulness in certain circumstances:

  1. It is the only way to share global functions across scripts.

  2. Julian API may not provide an interface unifying class-based and primitive types. For example, toString() can be used to convert any Object into a string, but you cannot call it against an int. It's natural to write a function with parameter of type Any to deal with the two cases together, but Julian's foundation classes do not provide such an API. Instead, Julian provides a built-in function which you can use by including its containing script, any.jul.

  3. It saves typing, a lot. By including print.jul you can start writing print(value) and println(value), which is much less verbose than Console.println(value). That said, we still think you should refrain from using global functions as much as possible. Julian will maintain a small set of global functions for daily use, but beyond that you will have to roll up your own.