Advanced Ludwig Syntax

This page focuses on advanced topics in Ludwig.

Click here to return to the Advanced Ludwig table of contents.

Parameterizing Compositions with Environment Variables

Ludwig supports the use of environment variables through the Ludwig.String getEnv function. The getEnv function takes the name of the environment variable as an argument and returns an Optional<String>.

In its simplest form, it might look like this:

path: getEnv("PATH")

Here, the environment variable is PATH. Note: In other languages, environment variables are often preceded by a dollar sign, but in Ludwig they are not.

Because the return type is an Optional<String>, you can use this function wherever a type expects a string, as long as you wrap it with a Ludwig.Optional function, such as unpack or unpackOrError.

Optional.unpack

Let’s demonstrate unpack first, with a simple example.

composition

import Fugue.AWS as AWS
import Fugue.AWS.EC2 as EC2

#########################
# NETWORKS
#########################

hello-world-vpc: EC2.Vpc.new {
  cidrBlock: "10.0.0.0/16",
  tags: [hello-world-tag],
  region: AWS.Us-west-2,
}

hello-world-tag: AWS.tag("Application", tag-value)

tag-value: Optional.unpack("Default", String.getEnv("TAG_VALUE"))

This is a spin on our Hello World composition. Instead of using “Hello World” as the tag value, we use the environment variable TAG_VALUE, wrapped by the Optional.unpack function.

Optional.unpack treats the first argument within the parentheses as the default value (the string “Default” above), and the second argument as the optional value to “unpack” (String.getEnv("TAG_VALUE") above). Since there are two possible states of an optional value – either it exists, or it does not exist – unpacking an optional value means that you are providing a value for either state. In this case, that means:

  • If the environment variable TAG_VALUE does not exist, then the result of the function is the string “Default”, and that’s what will appear in the VPC’s tag in the AWS Management Console.
  • If the environment variable TAG_VALUE does exist, then the result of the function is the value of the environment variable, and that’s what will appear in the VPC’s tag in the AWS Management Console. So, if you execute export TAG_VALUE='Hello World Tag', the tag value will be “Hello World Tag”, as in the diagram below.
Hello World Tag set with an environment variable.

Hello World Tag set with an environment variable.

Optional.unpackOrError

Another way to wrap getEnv is with the Optional.unpackOrError function, as shown below:

composition

import Fugue.AWS as AWS
import Fugue.AWS.EC2 as EC2

#########################
# NETWORKS
#########################

hello-world-vpc: EC2.Vpc.new {
  cidrBlock: "10.0.0.0/16",
  tags: [hello-world-tag],
  region: AWS.Us-west-2,
}

hello-world-tag: AWS.tag("Application", tag-value)

tag-value: Optional.unpackOrError(String.getEnv("TAG_VALUE"), "Error: No tag value set")

Optional.unpackOrError is similar to Optional.unpack. Again, there are two possible states for the optional value: it exists, or it doesn’t exist. However, unlike Optional.unpack, Optional.unpackOrError throws an error if the optional value doesn’t exist.

Optional.unpackOrError treats the first argument as the optional value to unpack (String.getEnv("TAG_VALUE") above), and the second argument as the error message to display (the string “Error: No tag value set” above).

  • If the environment variable TAG_VALUE does exist, then the result of the function is the value of the environment variable, and that’s what will appear in the VPC’s tag in the AWS Management Console.
  • If the environment variable TAG_VALUE does not exist, the Ludwig compiler will raise an error, like so:
$ fugue run EnvVarTest.lw

[ fugue run ] Running EnvVarTest.lw

Run Details:
    Account: default
    Alias: n/a

Compiling Ludwig file /Users/main-user/EnvVarTest.lw
[ ERROR ] Error compiling Ludwig composition:

ludwig (evaluation error):
  "/Users/main-user/EnvVarTest.lw" (line 19, column 12):
  error:

    19| tag-value: Optional.unpackOrError(String.getEnv("TAG_VALUE"), "Error: No tag value set")
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  Error: No tag value set

Using Environment Variables in Functions

It’s possible to use environment variables in functions. For example, see the module below:

fun getEnvironmentVar {
      name: String
    } -> String:
   Optional.unpackOrError(String.getEnv(name), "Environment variable not set")

x: String.print(getEnvironmentVar(name: 'GOPATH'))

This function, named getEnvironmentVar, takes an argument called name of the type String and returns a value of type String. We’re using Optional.unpackOrError here, so if the environment variable represented by name is set, the function returns the environment variable as a String. If the environment variable represented by name is not set, the function will return an error, “Environment variable not set.”

The last line in the module prints out the results of the function getEnvironmentVar(name), where name is the environment variable GOPATH. So, if you have set GOPATH in your terminal, you’ll see its value printed on the screen.

The above function uses named arguments, but you can also use tuples to create the same outcome, as seen below:

fun getEnvironmentVar(name: String) -> String:
   Optional.unpackOrError(String.getEnv(name), "Environment variable not set")

x: String.print(getEnvironmentVar('GOPATH'))

You can also introduce a Boolean flag into the function to make it a little more advanced:

fun getEnvironmentVar(name: String, defaults: Bool) -> String:
   if defaults then Optional.unpack("", String.getEnv(name))
   else Optional.unpackOrError(String.getEnv(name), "Environment variable not set")

x: String.print(getEnvironmentVar('GOPATH', True))

There are four possible outcomes with this function:

  • If the flag is set to True and the GOPATH environment variable is set, the value of GOPATH will be printed to the screen.
  • If the flag is set to True and GOPATH is not set, the default value (an empty string "") will be printed to the screen.
  • If the flag is set to False and GOPATH is set, the value of GOPATH will be printed to the screen.
  • If the flag is set to False and GOPATH is not set, the function will return an error, “Environment variable not set.”

This makes it easy to customize the behavior you want, all by flipping a flag.

Other Ways to Use Environment Variables

The above examples demonstrate using an environment variable to set the value in a tag key/value pair, or simply printing an environment variable to the screen. What else can you use environment variables with?

Ultimately, you can use an environment variable anywhere a string is expected, as long as it’s wrapped properly. In our Kubernetes the Hard Way example, you can see how we used Optional.unpackOrError to wrap a Route 53 ZONEID environment variable and an Elastic Load Balancer ELBNAME environment variable. We populate the environment variables by grabbing the desired information from fugue status, allowing the KubernetesDNS.lw composition to create a DNS alias for a hosted zone and point it to the ELB name.

String Templating

Ludwig allows you to create string templates so that you can build strings using values in Ludwig data structures. Ludwig implements a subset of the Mustache specification.

What is Mustache?

Mustache is a templating standard with an emphasis on simplicity. With Mustache, you place {{tags}} in a string (or a file loaded as a string), and then a hash of tag values, like {"tags": "foo-and-bar"}. A renderer – in this case, built into the Ludwig compiler – processes the template string and hash, and produces a string result. Mustache templates are “logic-less,” a term which is best explained in the Mustache manual:

We call it “logic-less” because there are no if statements, else clauses, or for loops. Instead there are only tags. Some tags are replaced with a value, some nothing, and others a series of values.

The Ludwig.Template module provides a subset of Mustache template rendering features.

How does Ludwig implement Mustache?

The render function in the Ludwig.Template module renders a string given a Mustache template and data.

In Ludwig, Mustache templates are given as strings bound to the template argument. Strings can be literal, composed from functions, and read from files.

Hashes of values are given as a record bound to the data argument. Record field values must be one of the following types:

  • Int
  • Float
  • String

Attempting to render data of any other of any other type is not supported and results in an error. By extension, this means that other data types – like Bool or custom types – must be converted to a supported value, such as a String.

More complex Mustache features, such as sections and lists, are supported. Information about these features can be found in the reference for the Ludwig.Template module. Lambdas are supported in that a Ludwig function can be used as a value for any field in a record. Partials are not supported.

Examples

This example shows a properly formed, non-missing example of Mustache templates in Ludwig.

import Ludwig.Template

ex1: Template.render {
  template: "Hello {{name}}!",
  data: {name: "Bob"},
  missBehavior: Template.MissAsEmptyString,
} # => "Hello Bob!"

This example shows a rendering “miss,” in that the template string specifies a field which is not defined in data (namely, {{name}}).

ex2: Template.render {
  template: "Hello {{name}}!",
  data: {age: 42},
  missBehavior: Template.MissAsEmptyString,
} # => "Hello !"

This example shows a similar miss, but the behavior for a miss has been set to MissAsError. As a result, compilation of a composition using this function would fail.

ex3: Template.render {
  template: "{{name}}!",
  data: {name1: "Bob"},
  missBehavior: Template.MissAsError,
} # => Substitution error: Variable not found: "name"

Parametric polymorphism

Warning

This is an advanced topic! Very few Fugue users will need to know about it.

Ludwig’s type system supports parametric polymorphism. This means that types and functions can be parameterized over one or more types.

The builtin List, Dictionary and Optional types are good examples:

# A list of ints.
List<Int> my-int-list: [1, 2, 3]

# A list of strings
List<String> my-string-list: ["Hello", "World"]

However, it is also possible to write your own parametric types:

type Pair<a, b>:
    fst: a
    snd: b

Pair<Int, String> my-pair:
    fst: 78
    snd: "Hello World!"

fun swap(p: Pair<a, b>) -> Pair<b, a>:
    fst: p.snd
    snd: p.fst

As you can see, type parameters are written in lowercase.

Note

Did you know that Fugue offers editor plug-ins so you can read and write Ludwig in your editor/IDE of choice? All plug-ins include Ludwig syntax highlighting, and some have additional features. See ludwig-mode for Emacs, ludwig-vim for vim, vscode-language-ludwig for VSCode, and language-ludwig for Atom.