Carina

Functions

Learn how to use built-in functions, define custom functions, and compose them with the pipe operator in Carina.

Carina provides built-in functions for common operations and lets you define your own functions. This guide covers both, along with the pipe and compose operators.

Built-in functions

String functions

FunctionSignatureDescription
upperupper(string) -> StringConverts to uppercase
lowerlower(string) -> StringConverts to lowercase
trimtrim(string) -> StringRemoves leading/trailing whitespace
replacereplace(search, replacement, string) -> StringReplaces all occurrences
splitsplit(separator, string) -> list(String)Splits string into a list
joinjoin(separator, list) -> StringJoins list elements into a string

Examples:

awscc.ec2.Vpc {
  cidr_block = '10.0.0.0/16'

  tags = {
    Name = upper('my-vpc')              # 'MY-VPC'
    Env  = replace('_', '-', 'my_env')  # 'my-env'
    Id   = join('-', ['vpc', 'prod'])    # 'vpc-prod'
  }
}

Collection functions

FunctionSignatureDescription
lengthlength(list | map | String) -> IntReturns element/character count
concatconcat(items, base_list) -> list(Any)Appends items to a list
flattenflatten(list) -> list(Any)Flattens nested lists by one level
keyskeys(map) -> list(String)Returns map keys as a sorted list
valuesvalues(map) -> list(Any)Returns map values sorted by key
lookuplookup(map, key, default) -> AnyLooks up a key with a fallback
mapmap(accessor, collection) -> list(Any) | map(Any)Extracts a field from each element

Examples:

let parts1 = ['web', 'test']
let parts2 = ['vpc']

awscc.ec2.Vpc {
  cidr_block = '10.0.0.0/16'

  tags = {
    Name = join('-', concat(parts2, parts1))  # 'web-test-vpc'
  }
}

Numeric functions

FunctionSignatureDescription
minmin(a, b) -> NumberReturns the smaller value
maxmax(a, b) -> NumberReturns the larger value

Network functions

FunctionSignatureDescription
cidr_subnetcidr_subnet(prefix, newbits, netnum) -> Ipv4CidrCalculates a subnet CIDR block

Example:

let vpcs = for (i, env) in ['dev', 'stg'] {
  awscc.ec2.Vpc {
    cidr_block = cidr_subnet('10.0.0.0/8', 8, i)
    # i=0 -> '10.0.0.0/16', i=1 -> '10.1.0.0/16'

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

Environment and security functions

FunctionSignatureDescription
envenv(name) -> stringReads an environment variable
secretsecret(value) -> secretMarks a value as secret (stored as hash in state)
decryptdecrypt(ciphertext, key?) -> stringDecrypts using the provider’s encryption service (e.g., AWS KMS)

User-defined functions

Define reusable logic with the fn keyword:

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

awscc.ec2.Vpc {
  cidr_block = '10.0.0.0/16'

  tags = {
    Name = tag_name('production', 'web')  # 'production-web-vpc'
  }
}

Function syntax

fn name(param1: type1, param2: type2): return_type {
  expression
}
  • Parameters can have type annotations (: String, : Int, etc.)
  • Return type annotation is optional (: String)
  • The function body is a single expression (the return value)

Local variables in functions

Functions can have local let bindings before the final expression:

fn subnet_name(env: String, tier: String, index: Int): String {
  let prefix = join("-", [env, tier])
  "${prefix}-${index}"
}

Default parameter values

Parameters can have default values:

fn make_tags(name: String, env: String = 'dev'): map(String) {
  {
    Name        = name
    Environment = env
  }
}

Pipe operator

The pipe operator |> passes the result of the left side as the last argument to the function on the right (data-last convention):

# Without pipe
let result = join('-', split('_', upper('hello_world')))

# With pipe -- reads left to right
let result = 'hello_world' |> upper() |> split('_') |> join('-')
# Result: 'HELLO-WORLD'

The pipe operator is especially useful with collection functions:

let names = ['web', 'api', 'worker']

# Extract and transform
let result = names |> join(', ')

Compose operator

The compose operator >> creates a new function by chaining two partially applied functions (closures). Both sides must be closures:

# split('_') is a closure (1 of 2 args provided)
# join('-') is a closure (1 of 2 args provided)
let transform = split('_') >> join('-')

The resulting function applies the left function first, then passes the result to the right function.

Partial application

When you call a built-in function with fewer arguments than it expects, you get a closure — a partially applied function that waits for the remaining arguments:

# replace expects 3 args; giving 2 returns a closure
let dashify = replace('_', '-')

# The closure is called when the last argument arrives (via pipe)
let result = 'hello_world' |> dashify()  # 'hello-world'

This works naturally with the pipe operator, since the piped value fills in the last argument.