Quick Jaskell Reference for Neptune Users
Neptune as a cross-project built tool, is based on a scripting language: jaskell. We believe it is hard to avoid clumsy syntax and lots of copy-paste with xml, so we decided to go with a scripting language. This reference gives some basic idea about what the jaskell language is and what you need to know to use it in writing your neptune build files.
It is not realistic to cover all details of the jaskell language in this short description. However, neptune users typically do not need to know a lot of the details anyway.
So, we'll start with a familiar "Frequently Asked Questions".
- Why Jaskell? Why not Python, Ruby, Groovy?
- Literals
- If-else
- variable and Function
- Tuple
- this variable
- Let clause
- Where clause
- Anonymous Function (AKA "lamda")
- Calling function with infix syntax
- String interpolation
- The "import" function
- The "module" function
- Loading Java Bean object
- Class loader and class path
- Loading Ant types
- calling Java functions
- neptune functions
- neptune target
- call ant task
- parameterized target and a typical solution for cross-project dependency
- Handling exception
- Going beyond Ant (The do-notation)
Why is Jaskell? Why not Python, Ruby, Groovy?
Neptune realizes the importance of combining small tasks to do real sophisticated work. Such combination involves communications between different tasks (or, command, in neptune's term).
In order to achieve simple syntax and flexible command combination, Neptune utilizes the "Monad" concept borrowed from the Haskell language. (Yes, that's where the name "jaskell" is stolen from). Python, Ruby, Groovy are all excellent Object-Oriented languages. Flexible and powerful as they are, they are not perfect natural fit to such functional idea. (Or, I should really say, I do not know how to do it with these languages)
Literals
Jaskell language supports literals that many other programming languages support: integer, decimal number, string, boolean.
For example: 1, 3.1, "abc", true, false.
Not surprisingly, jaskell support operators on these types of data, such as: 2+3, 1==1, 2>1, "abc"+"def" (which results in "abcdef").
Similar to ant, jaskell supports string interpolation. i.e. "my name is $myname" will evaluate to "my name is tom" when the value of variable "myname" equals to "tom". We'll come back to variables later.
Except these literals, jaskell support list and associative array ("tuple" in jaskell term).
- [] is an empty list
- [1,2,3] is a list of three numbers.
- {} is an empty tuple.
- {name="tom", age=10} is a tuple with two members.
Oh, by the way: comments in jaskell look the same as in Java.
if-else
Almost all language has if-else. if-else in jaskell is easy:
will evaluate to "ok".
Variable and Function
jaskell allows programmer to declare functions and variables (well, variables that cannot be changed). you can use a variable or call a function anywhere a literal can be used. For example:
There are two syntax to call a function:
- java-like syntax. like in "add(1,2)"
- haskell-like syntax. like in "add 1 2"
You can freely choose the syntax that you like and can even mix them. Actually, we did find that it is sometimes useful to mix them to get a less cluttered code.
A very important symbol (actually it is a function), is the dollar sign character: '$'. It is used to seperate sub-expressions.
Normally, when we want to call function f1 with the result of calling function f2 as the argument, we can say:
Such nested parenthesis can become ugly when the function call becomes more complex. The '$' operator can help in this case.
this expression is equivalent to f1(f2(a,b,c)), but it is less cluttered by the parenthesis.
Tuple
A tuple can be considered as an "object" because it uses the familiar "." to access its members.
evaluates to "tom".
evaluates to 50.
As you can see, both "," and ";" can be used to seperate tuple members.
Tuple can have both variable and function as its members. For example:
"this" variable
Let's first look at an example:
If you run the code, it will complain that it cannot find variable "firstname" and "lastname". This is because tuple members are not automatically visible to each other as in the case of Java class members.
This might look like a restriction, but it reduces chance of making mistake too. In order for the members to see each other, you have to use the special variable "this".
Let clause
Now let's look at how variables and functions are defined.
The above expression calculates the sum of a and b by defining the two variables first and then defining the add function.
Note that "let" is also an expression that evaluates to a value. The above let expression evaluates to 3.
Where clause
Let clause is typically used to enforce evaluation order. For function/variable declarations insensitive of evaluation order, "where" clause is used instead.
The above code does the same thing as the let expression. The good thing about "where" clause is, it has no requirement on declaration order for your variables/functions. This is ideal if the functions or variables reference each other.
Anonymous Function
Jaskell supports anonymous function, also known as the lamda function.
It may look tedious to say
In such case of a simple function that we don't want to bother giving it a name, we can do:
Calling function with infix sytnax
"add 1 2" looks ok, but not as natural as "1+2". This is partly because we are used to infix syntax for many binary operations.
Jaskell allows infix syntax for any function that takes 2 parameters. Simply prefix the function name with a back-apostrophe "`". For example:
Slightly better?
Not only simple variable name, expressions can be made infix as well:
Note, when infixing an expression more complex than "a.b.c" style, it needs to be enclosed with a pair of parenthesis.
String interpolation
Two syntax are provided for string interpolation
The two expressions are equivalent in this example. The first is simpler and the latter can be used to reference an expression more complex than a simple variable name. (such as "a.b.c+d").
import
jaskell code can be divided into small script files and these files can reference each other.
This will evaluate the expression stored in script2.jsl file.
module function
A more frequently used and possibly the most important function in neptune (a specially tailored version of jaskell) is the "module" function.
While "import" can be used to evaluate any expression in a script file, "module" is designed specially for loading neptune project files. So, neptune project files use "module" function to reference each other.
The above example is a typical project build file. it starts with a project declaration of names, basedir etc. And the project targets follow the next.
In this example, the project has only one target "build_project2", which in turn calls the build target in project2.
Loading Java Bean object
Function "declare" is designed to load and instantiate any Java Bean object.
For the following Java Bean class:
A Person object can be loaded into neptune with this code:
The declare function accepts 2 parameters, the first being the class name, and the second being the tuple with all Java Bean property values defined.
The printTomName task will call the ant "Echo" task and print the age of Tom.
Class loader and class path
It is pretty normal that we may have a class file or a jar file that is not known to Neptune when Neptune starts up.
In such case, explicit class path needs to be provided so that the "declare" function can find this class.
In order to do so, a ClassLoader object needs to be constructed. The "classloader" function creates a ClassLoader object that searches class from provided classpath:
The above code snippet creates a ClassLoader object by providing it a class path.
The "lib" folder is relative to the current basedir.
With a ClassLoader object available, the "declare" function can be called this way:
There are other options to create ClassLoader, such as specifying more than one path or location for the classpath; changing the default "parent-first" class loader strategy, etc.
But we won't go into that detail in this quick reference.
Loading Ant types
Data types and Tasks provided by Ant are also Java classes. They are special because Ant has a special pattern for constructing objects and setting up nested elements.
The syntax for loading Ant tasks and types are however very similar to that of loading regular java classes.
The "ant.declare" function can be used instead to load ant types and tasks. The good news is, parameters and usage of "ant.declare" is exactly the same as that of the "declare" function.
Suppose we have an Ant task class "com.qi.CookTask" to load, here's what needs to be done:
The above code searches class "com.qi.CookTask" in the given class path and constructs an Ant Task after the class is loaded.
Calling java functions
jaskell script can call java functions and reference java static fields and instance fields directly.
Referencing field is easy, it is exactly the same as what you do in Java:
Calling java function has a slightly different syntax though. i.e. Instead of enclosing the parameters in a pair of parenthesis, jaskell uses a pair of square brackets to enclose them.
As you might have guessed, jaskell puts all the arguments into a list and pass the list to the java function.
Yes, you are right. Jaskell has to do this because function currying and java function overloading do not live happily together.
Neptune functions
Neptune provides many functions and tuples out-of-box. Some examples:
- declare: loads a java class as a bean.
- now: gets the current time.
- sequence: run neptune commands sequentially.
- concurrent: run neptune commands concurrently.
- depends: declare dependency between targets.
- unless: the negation of "if".
- ant: the tuple that contains all ant related commands.
- properties: the tuple that contains all system properties when the runtime starts.
In this example,
- ant.declare is responsible for loading an Ant task class.
- mkdir is a function that calls the "mkdir" task of ant. By defining this convenient shortcut function, we save some key-strokes. Similarly, we defined function "echo" to save more key-strokes.
- Function "depends" declares the dependency between targets.
- Function "sequence" sequentially run the commands in the list.
Plus, all the standard jaskell utility functions are automatically available to aggressive neptune programmers. You need to feel comfortable with higher-order functions in order to use them.
neptune target
So, what are the targets in neptune project file? They are nothing special but tuple members of the second parameter of the "project" function.
In the above example, "build", "test" are the targets of the "neptune test" project.
calling ant task
The majority of the real work is done by calling ant tasks.
The standard and optional ant tasks are pre-built into the "ant" tuple.
You can call "ant.copy", "ant.javac", "ant.javadoc" etc to call them.
The following example copies a file:
Intuitively, the xml attributes of an ant task become the tuple members of the task in Neptune.
In order to attach sub-elements to a task or a type, use the "with" function:
The above example copies all jsl files from the source directory to the build directory.
Sub-elements are listed in the list parameter of the "with" function.
Sometimes, a sub-element has a name different than its type name, or maybe the type of the sub-element is not pre-loaded at all. In this case, the 'ant.element' function is used to dynamically load the sub-element.
The following example runs javadoc with specific "tag" information:
Function "ant.declare" is used to dynamically load custom Task class and ant data type.
Many ant tasks are placed in the 'ant' tuple for convenience. However, when any ant task is named something that's already being used by Neptune, for example, "declare", it will be shadowed by the neptune name. In such case, one can always use the 'ant.tasks' tuple to get the "declare" task.
The "ant.types" tuple contains pre-loaded ant types; the "ant.tasks" tuple contains pre-loaded ant tasks; the "ant.selectors" tuple contains pre-loaded ant selectors.
Parameterized target and a typical solution for cross-project dependency
Project targets are just tuple members, so they can be declared as functions that expect parameters. Such parameterized target is very useful in dealing with cross-project dependency.
Let's analyze a typical senario: I have project2 that requires a jar file created by project1. project1 resides in "home/project1" and project2 resides in "home/project2".
When building project2, I'd like to automatically build project1 and copy the project1.jar to my lib folder in project2's base directory.
And here's how a typical neptune solution will be: Firstly, project1 have a neptune build file named "build.np". And it looks like the following:
In order to export the jar file to other projects, we define a target named "deploy":
The target "dist" depends on the build target and puts all the class files into project1.jar.
Then, target "deploy" depends on the dist target to create the jar file, and then it copies the jar file to the provided directory parameter.
Finally, in "home/project2/build.np", we have our target "build" depend on the "deploy" target in project1.
Note, When jumping between projects, be careful about the directries and paths.
Should we not use the special variable "thismodule.thisdir", the "lib" would have been interpreted by project1 to be relative to its own base directory, which will be "home/project1/lib", rather than the expected "home/project2/lib".
When more projects are involved, it may be useful to create a global dependency file, save it in a publicly known place, and have all the individual projects reference this file.
For example, we could create a dependency.np file such as:
And then, we have project1 and project2 reference this file so that they don't have to directly point to each other.
This way, we can centrally manage dependencies between projects.
Handling exception
In the "infix syntax" section, we showed a "catch" function. And yes, you are right, it is for handling exceptions.
The above script tries to copy a file, and if the copy fails with a BuildException, it prints the exception message.
Slightly different than the Java counterpart, the "catch" function expects a function whose only parameter is the exception object.
Using the "catch" function, we can control exception directly within our neptune script.
Similarly, neptune provides a "finally" function and an "auto" function to do deterministic finalization.
The above code will not print "done" when the copy fails. This is because it failed. The job was not done.
But in case we do want certain action to be taken anyway, we can use "finally" as
Or use "auto" to place the "finalization block" first.
Going beyond Ant (The do-notation)
So far, we've been calling ant to do the real work. Ant provides rich functionality that covers most of the aspects of our daily work.
However, things get awkward when we need to "read" something instead of the typical "do something" in Ant.
The only way in Ant to "read" information is by setting and reading system property. This is kinda clumsy because of the syntax. More importantly, it is limited to only string.
Neptune designed a new concept called "Command". Command is a Java interface that one can implement to provide custom functionality to Neptune.
Command is very much like the Task class in Ant, and hopefully you will like to hear this:
it does not force you to "extends".
The biggest difference between Command and Task is however: "return value". Neptune commands can return object of any type to the script, enabling much more flexibility, while Task has no return value.
Let's look at an example.
Suppose I wanna make sure that file1's last modified date is earlier than that of file2. Also suppose there's a function named "readTimeStamp" that will return the current timestamp for a file:
The build target first reads timestamp from file1, then from file2, based on the two timestamp value, it then determines to either print "ok", or simply fail.
well, you might be wondering what the "do{ts1=...}" does by now.
Ladies and Gentlement, I give you: the do-notation.
Daladaladala...
Do-notation is part of the Monad algebra and is also stolen from the haskell language.
We'll not cover the mathematical meaning of it here.
But, quite intuitively, we can read
as * run some_command, collect the result in variable "var", and then run the following commands. *
In summary, the "sequence" function does simple sequencing of commands without caring about the return values, which is what we have always been doing in Ant.
The do-notation collects return value while running commands sequentially. This value can then be used in the subsequent logic.
Sounds more like a programming language now, does it?
Another possible usage of Command may be calling a web service to retrieve information and then doing actions according to the data returned from the web service
