NovahGithubSource

Syntax

Module definition

Every novah file should start with a module definition.

Begin code:

module my.namespace

...

End code.

Imports

Imports need to be defined after the module definition and before any other definitions. They can be qualified with the as keyword or given a list of names to import. Type constructors can be imported explicitly for a type or all at once with (..). Raw unqualified imports are not allowed except for core namespaces.

Begin code:

module my.namespace

import module1 as Mod
import module2 (fun1, Type1(Ctor1, Ctor2), Type2(..))

// error
import unqualified

End code.

Visibility

Declarations in Novah are private by default. The pub keyword should be used to create public declarations.

Begin code:

module visibility

// private declaration
foo : Int
foo = 3

// public declaration
pub
bar : Int -> String
bar x = toString x

End code.

The same is true for declared types, except the pub+ keyword can be used to also expose the constructors.

Begin code:

module visibility

// private type
type Foo = Foo Int

// public type with private constructors
pub
type Bar = Bar Int

// public type with public constructors
pub+
type Baz = Baz Int

End code.

Entry point

Any namespace containing a public function called main with type Array String -> Unit will be compiled into a runnable jvm class.

Begin code:

module entrypoint

pub
main : Array String -> Unit
main args =
  println ("Hello " ++ args.[0])

End code.

Functions

Functions and values can be easily defined. All values in Novah are immutable, so there are no global mutable variables. All top level definitions must start with a lower case character.

Begin code:

module functions

fun1 : String -> Boolean
fun1 str = str == "true"

val1 : Float64
val1 = 3.1415

// Top level definitions require explicit types
// unless they have a `noWarn` attribute
// which can also be put at the module level
#[noWarn]
times10 x = x * 10

End code.

Algebraic data types (ADTs)

Types can be defined with the keyword type. A type can have multiple constructors (sum type) and multiple parameters (product type). Both types and constructors must start with an upper case character.

Begin code:

module types

// Types can be parameterized
type Option a = Some a | None

// Simple enumeration
type Color = Red | Black

// A recursive type
type LinkedList a = Nil | Cons a (LinkedList a)

End code.

Type aliases

An alias to a type can be created with the typealias keyword. Aliases are just syntatic sugar and are considered the same type as their alias by the compiler. Int is a typealias to Int32.

Begin code:

module types2

typealias MyBoolean = Boolean

// Type aliases can have parameters
typealias Maybe a = Option a

// Aliases cannot be recursive
typealias Wrong a = Option (Wrong a)

End code.

Strings

Strings can be created with double quotes like in Java or with triple quotes for unescaped, multi-line strings.

Begin code:

#[noWarn]
module strings

str1 = "hello world\n"

str2 = """Triple quoted
strings can span multiple lines
and don't consider any kind of escape like \n \t
"""

End code.

Regular expression patterns

Novah has support for literal regex patterns. Literal regexes won't escape characters like normal strings, except for double quotes (").

Begin code:

module regexes

#[noWarn]
foo : Unit -> Unit
foo () =
  let [_, num] = Re.find #"(\d+)" "12345"
  println (int num)

End code.

Conditional expressions

Novah has two conditional expressions: if and case.

Begin code:

module conditionals

// Both the then and else cases must have the same type
abs : Int -> Int
abs x = if x > 0 then x else negate x

// An if without an else must always return Unit
printPositive : Int -> Unit
printPositive x =
  if x > 0 then
    println x

mapList : (a -> b) -> List a -> List b
mapList f list = case list of
  [] -> []
  [x :: xs] -> [f x] ++ mapList f xs

End code.

See pattern matching.

Atoms

Sometimes you need to mutate values to achieve better Java interoperability or a more idiomatic code. Novah has atoms for these situations.

Atoms are synchronous atomic values to manage independent state. They can be dereferenced, changed and reset. They should always hold immutable values and the swap function has to be free of side effects, as it may run multiple times.

Begin code:

module atoms

pub
main : Array String -> Unit
main _ =
  // create and atom
  let val = atom 10
  // dereference an atom
  println @val
  // reset the value
  val := 99
  reset 99 val
  // apply a function the value
  val ::= (_ * 10)
  swap (_ * 10) val

End code.

While loop

Even though Novah support while loops, a tail recursive function is the idiomatic way to represent such constructs.

Begin code:

module loops

// Not idiomatic
factorialIterative : Int -> Int
factorialIterative x =
  let fact = atom x
  let i = atom x

  while @i > 1 do
    swap (_ - 1) i
    swap (_ * @i) fact
  @fact

// Idiomatic version
// Novah will optimize the tail recursion to a loop
factorialRecursive : Int -> Int
factorialRecursive x =
  let fact num acc = case num of
    0 -> acc
    1 -> acc
    n -> fact (n - 1) (acc * n)
  fact x 1

// Another way to calculate a factorial
factorial : Int32 -> Int32
factorial x =
  List.product [1 .. x]

End code.

Operators

It's possible to define new operators in Novah.

Begin code:

module operators

(?!) : Option a -> a -> a
(?!) opt default = case opt of
  Some x -> x
  None -> default

or0 : Option Int -> Int
or0 n = n ?! 0

End code.

Any function can be used as an operator by surrounding it with `, making it a backtick operator.

Begin code:

module operators

sayNumber : Int -> String
sayNumber x = "the number is: %d" `format` [x]

End code.

Associativity

OperatorAssociativity
<|Right
$ starting operatorsRight
: starting operatorsRight
everything elseLeft

Precedence

Operator (first character)ExamplePrecedence
;(3 ; 5)0
$-1
|true || false2
&true && false3
=, !3 == 1, string ! index4
>, <5 > 1, 3 <= 75
+, -, :, ?1 + 1, head :: tail, option ? default6
*, /, %3 * 9, 5 / 27
^, .[1 ... 10], 'a' .. 'z'8
`(3 + 3) `shouldBe` 69

Ranges

Ranges are just normal user-defined operators in Novah. There is syntactic sugar for creating ranges of lists and sets.

Begin code:

module ranges

// Closed range [x y]
zeroToTen : Range Int
zeroToTen = 1 .. 10

// Open range [x y)
zeroToNine : Range Int
zeroToNine = 1 ... 10

digitList : List Int
digitList = [0 .. 9]

lowerChars : Set Char
lowerChars = #{'a' .. 'z'}

// A step can be given to the range function
oddDigits : Range Int
oddDigits = range 1 10 2

pub
main : Array String -> Unit
main _ =
  forEachRange oddDigits println

End code.

Collections

Novah comes with literal support for commonly used data structures like lists, sets, tuples and (row-polymorphic) records. All Novah data structures are immutable and persistent, using bifurcan.

Begin code:

module collections

aList : List Int
aList = [1, 2, 3, 4, 5]

aSet : Set Char
aSet = #{'a', 'b', 'c'}

aTuple : Tuple Boolean Char
aTuple = true ; 'z'

aRecord : { x : Float64, y : Float64 }
aRecord = { x: 1.2, y: 5.66 }

// Novah also has support for persistent maps
// but there's no literal syntax for it
aMap : Map String Int
aMap = Map.new ["a" ; 0, "b" ; 1, "c" ; 2]

End code.

Lists in Novah are implemented using RRB (Relaxed-Radix-Balanced) vectors so they can be used as linked lists or vectors without any performance penalty.

See records.

Type annotations and casts

Novah requires almost no type annotations but values can still be annotated with a type.

Begin code:

module annotations

foo : Unit -> Int
foo () =
  // annotate a value
  let pi = Math.pi : Float64
  // compilation error: types don't match
  let piInt = Math.pi : Int
  // annotate a function parameter
  let fun (x : Int) = x + 1
  // fully annotate a let
  let fun2 : Int -> String
      fun2 x = show (x + 1)

End code.

Top level values are required to be annotated for documentation purpose unless a #[noWarn] attribute is present.

Begin code:

module annotations

// compilation error: no type annotation
foo () = 1

// OK
#[noWarn]
bar () = 1

End code.

Types can also be cast to some other types with the as keyword. Casts should only be used for Java interoperability as they can cause runtime errors!

Begin code:

module casts

pub
hashCode : a -> Int32
hashCode x = (x as Object)#hashCode()

End code.

Optional values

Novah has a type for representing optional values: Option a. This type is converted to a nullable Java object (or a boxed type in case of primitives) for optimization and interoperability purposes.

Begin code:

module options

// options can be pattern matched
hasValue : Option a -> Boolean
hasValue option = case option of
  Some _ -> true
  None -> false

// ?: can be used to return default values from an option
or0 : Option Int -> Int
or0 x = x ?: 0

// ?? can be used to execute functions inside an option,
// like `Options.map`
thread : Option (List Int) -> Option Int
thread list = list ?? List.map (_ + 1) ?? List.sum

End code.

Options can be unwrapped with the !! operator. Note that this is an unsafe operation and should be used only in tests or throw-away code!

Begin code:

module options

pub
main : Array String -> Unit
main _ =
  // unwrap the option as the list is not empty
  println (List.min [5, 9, 3, 6, 1, 8])!!
  // _!! can be used as a function that unwraps an option
  [5, 9, 3, 6, 1, 8]
  |> List.max
  |> _!!
  |> println

End code.

Threading functions

In object oriented language it's common to chain methods using dot syntax. Ex.: obj.method1().method2(). In Novah the idiomatic way to achieve this is the railway operator |> and its friends: <|, <<, >>.

Threading operators

OperatorNameExample
|>railway"56" |> String.reverse |> int
<|reverse railwayint <| String.reverse <| "56"
>>forward function compositionString.split "," >> List.size
<<backward function compositionList.size << String.split ","

Begin code:

module railway

throwDice : Int -> Int -> Int
throwDice faces modifier =
  [1 .. faces]
  |> List.randNth
  |> (_ + modifier)

throwD6 : Int -> Int
throwD6 = throwDice 6

parseNumbers : String -> List Int
parseNumbers = String.split "," >> List.map int

End code.

Anonymous function arguments

Novah support unamed lambda arguments in some specific cases using _ (underscore) as the function parameter.

Begin code:

module anonymouslambdas

foreign import java.lang.Math

// in operators
plus1 : List Int -> List Int
plus1 list = List.map (_ + 1) list

// in ifs
ifs : Boolean -> Int
ifs = if _ then 1 else -1

ifs2 : Int -> a -> a -> a
ifs2 num = if num == 0 then _ else _

// in cases
cases : List a -> String
cases = case _ of
  [] -> "empty"
  [_] -> "one"
  _ -> "more"

cases2 : Option Int -> Option Int -> Int
cases2 = case _, _ of
  Some x, Some y -> x + y
  Some x, None -> x
  None, Some y -> y
  None, None -> -1

// in record access
names : List { name : String } -> List String
names = List.map _.name

// in record construction
person : String -> Int -> { age : Int, name : String }
person = { name: _, age: _ }

// in native function arguments
exps : List Float64
exps = List.map Math#exp(_) [3.0, 5.0, 9.0]

End code.

Exceptions

Novah ditches exceptions in favor of type-safe errors using the Result type, but for Java interoperability purposes, exceptions are also supported.

Throwing exceptions

Begin code:

module throwing

foreign import java.lang.RuntimeException

foo : Unit -> a
foo () =
  throw RuntimeException#new("oops")

End code.

Catching exceptions

Begin code:

module catching

foreign import java.lang.Exception
foreign import java.lang.Throwable

foo : DangerClass -> Unit
foo object =
  try
    println object#dangerousMethod()
  catch
    :? Exception as e -> println e#getMessage()
    :? Throwable -> println "a throwable error"
  finally
    println "it's over"

End code.

Using Result

Begin code:

module resulterrors

foreign import java.io.File
foreign import java.io.IOException

createFile : String -> Result Boolean String
createFile name =
  try
    Ok File#new(name)#createNewFile()
  catch
    :? IOException as ioe -> Err ioe#getMessage()

foo : Unit -> Unit
foo () =
  case createFile "/tmp/myFile" of
    Ok _ -> println "worked"
    Err err -> println err

End code.