Carina

Expressions

For loops, if/else conditionals, let bindings, pipe operator, compose operator, and function definitions in the Carina DSL.

Expressions in the Carina DSL produce values. They can appear as attribute values, function arguments, or in any context where a value is expected.

For Expression

The for expression iterates over a list or map to create multiple resources or values.

Iterating Over a List

let vpcs = for env in ['dev', 'stg'] {
  awscc.ec2.Vpc {
    cidr_block = '10.0.0.0/16'

    tags = {
      Name        = "vpc-${env}"
      Environment = env
    }
  }
}

Indexed Iteration

Use (index, value) to access both the index and the element:

let vpcs = for (i, env) in ['dev', 'stg'] {
  awscc.ec2.Vpc {
    cidr_block = cidr_subnet('10.0.0.0/8', 8, i)

    tags = {
      Name        = "for-list-test-${env}"
      Environment = env
    }
  }
}

Iterating Over a Map

Use key, value to iterate over map entries:

let cidrs = {
  dev = '10.0.0.0/16'
  stg = '10.1.0.0/16'
}

let vpcs = for name, cidr in cidrs {
  awscc.ec2.Vpc {
    cidr_block = cidr

    tags = {
      Name        = "vpc-${name}"
      Environment = name
    }
  }
}

Local Bindings in For Body

let bindings can be used inside a for body to compute intermediate values:

let networks = for name, cidr in cidrs {
  let subnet_cidr = cidr_subnet(cidr, 8, 1)

  network {
    cidr_block  = cidr
    subnet_cidr = subnet_cidr
    az          = 'ap-northeast-1a'
  }
}

For with Module Calls

for works with module calls to create multiple instances of a module. See Modules: Modules with For Expressions for a full example.

If Expression

The if expression conditionally produces a resource or value.

Conditional Resource Creation

let is_production = true

if is_production {
  awscc.ec2.NatGateway {
    allocation_id = eip.allocation_id
    subnet_id     = subnet.subnet_id
  }
}

If/Else as a Value

if/else can be used inline to choose between values:

awscc.ec2.Vpc {
  cidr_block = if is_production { '10.0.0.0/16' } else { '172.16.0.0/16' }

  tags = {
    Name = if is_production { 'prod-vpc' } else { 'dev-vpc' }
  }
}

Local Bindings in If Body

let bindings can be used inside if and else blocks:

if is_production {
  let cidr = '10.0.0.0/16'

  awscc.ec2.Vpc {
    cidr_block = cidr
  }
}

Let Binding

let binds a name to a value. At the top level, it binds resources or values. Inside blocks, it creates scoped variables.

# Top-level value binding
let env = 'prod'
let zones = ['ap-northeast-1a', 'ap-northeast-1c']

# Top-level resource binding
let vpc = awscc.ec2.Vpc {
  cidr_block = '10.0.0.0/16'
}

# Module use binding
let network = use { source = './modules/network' }

Upstream state is bound with let <binding> = upstream_state { source = "..." }. See Upstream State for details.

Use let _ = (the discard pattern) when you need to evaluate an expression but do not need to reference the result. See Syntax: Discard Pattern for details.

Pipe Operator (|>)

The pipe operator passes the result of the left expression as the last argument to the function on the right. This enables readable left-to-right data transformations.

# Without pipe: nested calls are hard to read
let result = join('-', concat(['vpc'], ['prod', 'web']))

# With pipe: reads left to right
let result = ['prod', 'web'] |> concat(['vpc']) |> join('-')

The pipe operator works with any built-in function. The piped value becomes the last argument:

# These are equivalent:
join('-', ['a', 'b', 'c'])
['a', 'b', 'c'] |> join('-')

# These are equivalent:
replace('-', '_', 'hello-world')
'hello-world' |> replace('-', '_')

# These are equivalent:
split(',', 'a,b,c')
'a,b,c' |> split(',')

Chaining Multiple Pipes

Multiple pipe operations can be chained for complex transformations:

let result = ['prod', 'web']
  |> concat(['vpc'])
  |> join('-')
  |> upper()

Compose Operator (>>)

The compose operator (>>) combines two partially applied functions into a new function. The result of the first function is passed as input to the second. Both sides of >> must be closures (partially applied functions).

# Compose split and join into a single function
let transform = split(',') >> join('-')

# Apply the composed function via pipe
let result = 'a,b,c' |> transform()
# => 'a-b-c'

Compose works with any partially applied built-in function:

# Extract IDs from a list of maps, then join them
let pipeline = map('.id') >> join(', ')
let result = [{ id = '1' }, { id = '2' }] |> pipeline()
# => '1, 2'

Three or more functions can be composed:

let transform = split(',') >> join('-') >> split('-')

The compose operator binds tighter than the pipe operator, so f >> g |> h means (f >> g) |> h.

Function Calls

Functions are called with parentheses:

let len = length(zones)
let subnet = cidr_subnet('10.0.0.0/16', 8, 1)
let name = join('-', ['prod', 'web', 'vpc'])

Partial Application

When a built-in function is called with fewer arguments than it expects, it returns a closure that captures the provided arguments. The closure waits for the remaining arguments before executing.

# split expects 2 args: split(separator, string)
# Providing only 1 creates a closure
let split_by_comma = split(',')

# The closure can be used with pipe (parentheses are required)
let parts = 'a,b,c' |> split_by_comma()

This is particularly useful with the pipe operator:

let result = 'hello-world' |> replace('-', '_')
# replace(search, replacement, string) gets '-' and '_' captured,
# then 'hello-world' is supplied as the third argument via pipe

User-Defined Functions

Define functions with fn. Parameters can have optional type annotations and default values:

fn tag_name(env: String, service: String): String {
  join('-', [env, service, 'vpc'])
}

Parameters

Function parameters support:

  • Type annotations: name: String
  • Default values: name: String = "default"
  • No annotation: name (any type accepted)
fn make_tags(env: String, service: String, team: String = 'platform') {
  {
    Environment = env
    Service     = service
    Team        = team
  }
}

Local Bindings in Functions

Functions can contain let bindings before the return expression:

fn subnet_name(env: String, az: String): String {
  let short_az = replace('ap-northeast-1', '', az)
  "${env}-subnet-${short_az}"
}

Return Type

The optional return type annotation follows the parameter list with a colon:

fn cidr_for_env(env: String): String {
  lookup({ dev = '10.0.0.0/16', stg = '10.1.0.0/16' }, env, '10.99.0.0/16')
}

Validate Expressions

Validate expressions are boolean expressions used in arguments blocks for input validation and in require statements. They support comparison and logical operators.

Comparison Operators

OperatorMeaning
==Equal
!=Not equal
>Greater than
<Less than
>=Greater than or equal
<=Less than or equal

Logical Operators

OperatorMeaning
&&Logical AND
||Logical OR
!Logical NOT

Example: Argument Validation

arguments {
  instance_count: Int {
    description = 'Number of instances to create'
    default     = 1
    validation {
      condition     = instance_count >= 1 && instance_count <= 10
      error_message = 'Instance count must be between 1 and 10'
    }
  }
}

Function calls can be used in validate expressions:

arguments {
  name: String {
    validation {
      condition     = length(name) > 0
      error_message = 'Name must not be empty'
    }
  }
}