A step-by-step guide that builds a real command-line weather tool. Each step introduces one language concept and produces runnable code. By the end you'll have a working program that fetches live weather data, parses JSON, handles errors, and writes reports to disk.
A CLI tool that takes a city name, fetches current weather from the free Open-Meteo API, and displays temperature, wind speed, and conditions. Along the way you'll learn variables, functions, HTTP, JSON parsing, pattern matching, records, environment variables, file I/O, and error handling — the real building blocks of any Nulang program.
Prerequisites: Nulang installed (nulang --version). Network access.
Create a new Nulang project (or work with a single file — we'll use a standalone script to keep things simple):
mkdir weather-cli && cd weather-cliCreate weather.nula — your entry point. We'll build it up step by step.
Package manager alternative:
nulang nula new weather-cliscaffolds a full package withNulang.tomlandsrc/main.nula. The code in this tutorial works identically in either layout.
Verify everything works:
nulang --eval 'perform IO.print("Ready!")'Expected output:
Ready!
Open weather.nula and write your first line:
perform IO.print("Hello, Weather CLI!")
Run it:
nulang weather.nulaExpected output:
Hello, Weather CLI!
What's happening: IO.print is a built-in effect. The perform
keyword tells Nulang "execute this effect now." Built-in effects (IO,
Http, FS, System, Env) are wired into the VM — no import needed.
Every effect call requires perform.
Nulang uses let for immutable bindings. Use + to join strings:
let city = "Berlin"
let message = "Weather for " + city + ":"
perform IO.print(message)
Run it:
nulang weather.nulaExpected output:
Weather for Berlin:
Key points:
letbindings can't be reassigned. Usevarif you need mutation.+concatenates strings; useperform Int.to_string(n)orperform Float.to_string(n)to convert numbers first.- No semicolons needed. Blocks use
{ }for grouping.
Replace
weather.nulawith just this code — we'll rebuild it at each step.
Functions are values. Define them with fn(params) { body }:
let greet = fn(city: String) {
perform IO.print("Weather for " + city + ":")
}
greet("Berlin")
greet("Tokyo")
Run it:
Weather for Berlin:
Weather for Tokyo:
You can also write a top-level function with fn name(params) { ... }:
fn greet(city: String) {
perform IO.print("Weather for " + city + ":")
}
greet("Berlin")
Both forms are equivalent. The fn(name) { body } closure form is handy for
passing functions as arguments; the fn name(params) { body } form is
idiomatic for top-level definitions.
Functions return the last expression in their body. Explicit return is
not needed.
Let's read a city name from the command line. System.arg(n) returns the
n-th argument, or nil if none was provided:
let city = perform System.arg(2)
perform IO.print("City: " + city)
Run with and without an argument:
nulang weather.nula Berlin
# → City: Berlin
nulang weather.nula
# → City: nilArgument indexing: System.arg(0) is the program name, System.arg(1)
is the script path, and System.arg(2) is the first user argument.
When no argument is given, city is nil — Nulang's sentinel for "no
value." We'll handle that next.
Use if ... then ... else to handle missing input gracefully:
let city = perform System.arg(2)
let has_city = city != nil
if has_city then {
perform IO.print("Fetching weather for " + city + "...")
} else {
perform IO.print("Usage: nulang weather.nula <city>")
perform IO.print("")
perform IO.print("Example:")
perform IO.print(" nulang weather.nula Berlin")
}
Run it:
nulang weather.nula
# → Usage: nulang weather.nula <city>
# →
# → Example:
# → nulang weather.nula Berlin
nulang weather.nula Berlin
# → Fetching weather for Berlin...Key points:
- Conditions don't need parentheses:
has_citynot(has_city). thenseparates the condition from the true branch.!= nilchecks if a value exists;== nilchecks if it doesn't.
This tutorial uses the free Open-Meteo API (no key required). But for
production apps you might want a paid service with an API key. Let's
teach Env.get with an optional key:
let city = perform System.arg(2)
let has_city = city != nil
if has_city then {
// Check for an optional API key
let api_key = perform Env.get("WEATHER_API_KEY")
let has_key = api_key != nil
if has_key then {
perform IO.print("Using API key: " + api_key)
} else {
perform IO.print("No API key set — using free tier")
}
perform IO.print("Fetching weather for " + city + "...")
} else {
perform IO.print("Usage: nulang weather.nula <city>")
}
Try it:
nulang weather.nula Berlin
# → No API key set — using free tier
# → Fetching weather for Berlin...
WEATHER_API_KEY=abc123 nulang weather.nula Berlin
# → Using API key: abc123
# → Fetching weather for Berlin...Key point: Env.get returns nil when the variable isn't set. Use
!= nil to check and provide a fallback.
We'll use the Open-Meteo API, which takes latitude and longitude. Rather than require the user to type coordinates, let's build a small city lookup table:
let city = perform System.arg(2)
let has_city = city != nil
if has_city then {
// City → coordinates lookup
let lat = if city == "Berlin" then "52.52"
else if city == "Tokyo" then "35.68"
else if city == "New York" then "40.71"
else if city == "London" then "51.51"
else "52.52" // default to Berlin
let lon = if city == "Berlin" then "13.41"
else if city == "Tokyo" then "139.76"
else if city == "New York" then "-74.01"
else if city == "London" then "-0.13"
else "13.41"
// Build the API URL
let url = "https://api.open-meteo.com/v1/forecast" +
"?latitude=" + lat +
"&longitude=" + lon +
"¤t_weather=true"
perform IO.print("URL: " + url)
} else {
perform IO.print("Usage: nulang weather.nula <city>")
}
Run it:
nulang weather.nula Tokyo
# → URL: https://api.open-meteo.com/v1/forecast?latitude=35.68&longitude=139.76¤t_weather=trueWhat we did: built a URL with query parameters using string concatenation.
+ chains work across multiple lines — Nulang reads them as one expression.
In the bonus section we'll add real geocoding via an API call, eliminating the hardcoded lookup table.
Now let's actually fetch data. Http.get(url) returns the response body
as a string — or nil if the request fails:
let city = perform System.arg(2)
let has_city = city != nil
if has_city then {
let lat = if city == "Berlin" then "52.52"
else if city == "Tokyo" then "35.68"
else if city == "New York" then "40.71"
else if city == "London" then "51.51"
else "52.52"
let lon = if city == "Berlin" then "13.41"
else if city == "Tokyo" then "139.76"
else if city == "New York" then "-74.01"
else if city == "London" then "-0.13"
else "13.41"
let url = "https://api.open-meteo.com/v1/forecast" +
"?latitude=" + lat +
"&longitude=" + lon +
"¤t_weather=true"
perform IO.print("Fetching weather for " + city + "...")
let response = perform Http.get(url)
let ok = response != nil
if ok then {
let size = perform String.length(response)
perform IO.print("Received " + perform Int.to_string(size) + " bytes")
perform IO.print(response)
} else {
perform IO.print("Error: could not reach the weather API.")
perform IO.print("Check your network connection and try again.")
}
} else {
perform IO.print("Usage: nulang weather.nula <city>")
}
Run it — you should see raw JSON:
nulang weather.nula Berlin
# → Fetching weather for Berlin...
# → Received 235 bytes
# → {"latitude":52.52,"longitude":13.41,"current_weather":{"temperature":...Key points:
perform Http.get(url)hits the network — it's an effect.response != nilchecks if the request succeeded.perform String.length(s)returns the byte count of a string.perform Int.to_string(n)converts an integer for printing.
Import stdlib::json to get parse, stringify, get_string, and
get_number. The parse function turns a JSON string into a JsonValue:
import stdlib::json
let city = perform System.arg(2)
let has_city = city != nil
if has_city then {
let lat = if city == "Berlin" then "52.52"
else if city == "Tokyo" then "35.68"
else if city == "New York" then "40.71"
else if city == "London" then "51.51"
else "52.52"
let lon = if city == "Berlin" then "13.41"
else if city == "Tokyo" then "139.76"
else if city == "New York" then "-74.01"
else if city == "London" then "-0.13"
else "13.41"
let url = "https://api.open-meteo.com/v1/forecast" +
"?latitude=" + lat +
"&longitude=" + lon +
"¤t_weather=true"
perform IO.print("Fetching weather for " + city + "...")
let response = perform Http.get(url)
let ok = response != nil
if ok then {
let parsed = parse(response)
let pretty = stringify(parsed)
perform IO.print("Parsed JSON: " + pretty)
} else {
perform IO.print("Error: could not reach the weather API.")
}
} else {
perform IO.print("Usage: nulang weather.nula <city>")
}
Module imports use :: (not .): import stdlib::json. Imported names
are unqualified — call parse(...) and stringify(...) directly.
Important: Run from the project root so the stdlib path is resolvable. In development, set
NULANG_STDLIBto point atsrc/stdlib/. The examples usenulang weather.nulafrom the project root.
JsonValue is a variant type with six constructors. Use match to
inspect which kind of value you have and extract data:
import stdlib::json
let city = perform System.arg(2)
let has_city = city != nil
if has_city then {
let lat = if city == "Berlin" then "52.52"
else if city == "Tokyo" then "35.68"
else if city == "New York" then "40.71"
else if city == "London" then "51.51"
else "52.52"
let lon = if city == "Berlin" then "13.41"
else if city == "Tokyo" then "139.76"
else if city == "New York" then "-74.01"
else if city == "London" then "-0.13"
else "13.41"
let url = "https://api.open-meteo.com/v1/forecast" +
"?latitude=" + lat +
"&longitude=" + lon +
"¤t_weather=true"
let response = perform Http.get(url)
let ok = response != nil
if ok then {
let parsed = parse(response)
// Classify and extract based on JSON shape
let summary = match parsed {
JsonNull => "Response was null — unexpected!",
JsonBool(b) => if b then "true" else "false",
JsonNumber(n) => "number: " + perform Float.to_string(n),
JsonString(s) => "string (" +
perform Int.to_string(perform String.length(s)) +
" chars)",
JsonArray(a) => "array with " +
perform Int.to_string(perform Array.length(a)) +
" items",
JsonObject(_) => "weather object"
}
perform IO.print("Result: " + summary)
} else {
perform IO.print("Error: could not reach the weather API.")
}
} else {
perform IO.print("Usage: nulang weather.nula <city>")
}
Run it:
nulang weather.nula Berlin
# → Result: weather objectHow match works: Each arm Pattern => expression tests the value.
Patterns bind variables — JsonObject(_) matches any object, while
JsonString(s) binds the string payload to s. The underscore _
discards a binding you don't need.
Nulang also supports
match value with { | pattern => ... }syntax with leading|and guards (if). We use the compact form here.
Records group related data with named fields. Let's define a Weather
record and populate it:
import stdlib::json
// A Weather record to hold structured data
let make_weather = fn(city: String, temp: Float, wind: Float, code: Int) {
{ city: city, temp: temp, wind: wind, weather_code: code }
}
let city = perform System.arg(2)
let has_city = city != nil
if has_city then {
let lat = if city == "Berlin" then "52.52"
else if city == "Tokyo" then "35.68"
else if city == "New York" then "40.71"
else if city == "London" then "51.51"
else "52.52"
let lon = if city == "Berlin" then "13.41"
else if city == "Tokyo" then "139.76"
else if city == "New York" then "-74.01"
else if city == "London" then "-0.13"
else "13.41"
let url = "https://api.open-meteo.com/v1/forecast" +
"?latitude=" + lat +
"&longitude=" + lon +
"¤t_weather=true"
let response = perform Http.get(url)
let ok = response != nil
if ok then {
let parsed = parse(response)
// For now, use hardcoded values; we'll extract from JSON next
let weather = make_weather(city, 21.5, 12.3, 2)
perform IO.print("City: " + weather.city)
perform IO.print("Temp: " + perform Float.to_string(weather.temp) + "°C")
perform IO.print("Wind: " + perform Float.to_string(weather.wind) + " km/h")
perform IO.print("Code: " + perform Int.to_string(weather.weather_code))
} else {
perform IO.print("Error: could not reach the weather API.")
}
} else {
perform IO.print("Usage: nulang weather.nula <city>")
}
Record syntax:
- Creation:
{ field: value, ... }— using colons. - Field access:
record.field— using dots. - Update:
{ base .. field = new_val }— using..and=.
Now let's format the weather data into a readable report. We'll convert the numeric weather code into a human-readable description:
import stdlib::json
let make_weather = fn(city: String, temp: Float, wind: Float, code: Int) {
{ city: city, temp: temp, wind: wind, weather_code: code }
}
// Map WMO weather codes to descriptions
let describe_code = fn(code: Int) {
if code == 0 then "Clear sky"
else if code <= 3 then "Partly cloudy"
else if code <= 48 then "Fog"
else if code <= 55 then "Drizzle"
else if code <= 65 then "Rain"
else if code <= 75 then "Snow"
else if code <= 82 then "Rain showers"
else if code <= 99 then "Thunderstorm"
else "Unknown"
}
let city = perform System.arg(2)
let has_city = city != nil
if has_city then {
let lat = if city == "Berlin" then "52.52"
else if city == "Tokyo" then "35.68"
else if city == "New York" then "40.71"
else if city == "London" then "51.51"
else "52.52"
let lon = if city == "Berlin" then "13.41"
else if city == "Tokyo" then "139.76"
else if city == "New York" then "-74.01"
else if city == "London" then "-0.13"
else "13.41"
let url = "https://api.open-meteo.com/v1/forecast" +
"?latitude=" + lat +
"&longitude=" + lon +
"¤t_weather=true"
let response = perform Http.get(url)
let ok = response != nil
if ok then {
let parsed = parse(response)
// Placeholder values — we'll extract real ones in Step 16
let weather = make_weather(city, 21.5, 12.3, 2)
// Build a formatted report
let report =
"══════════════════════════\n" +
" Weather for " + weather.city + "\n" +
"══════════════════════════\n" +
" Temperature: " + perform Float.to_string(weather.temp) + " °C\n" +
" Wind speed: " + perform Float.to_string(weather.wind) + " km/h\n" +
" Conditions: " + describe_code(weather.weather_code) + "\n" +
"══════════════════════════"
perform IO.print(report)
} else {
perform IO.print("Error: could not reach the weather API.")
}
} else {
perform IO.print("Usage: nulang weather.nula <city>")
}
Run it:
══════════════════════════
Weather for Berlin
══════════════════════════
Temperature: 21.5 °C
Wind speed: 12.3 km/h
Conditions: Partly cloudy
══════════════════════════
Conversion functions: Float.to_string and Int.to_string are
built-in effects — always call them with perform. They convert numbers
to strings for display or concatenation.
The weather API can return errors (bad coordinates, rate limiting, etc.). Let's check for error indicators in the response:
import stdlib::json
let city = perform System.arg(2)
let has_city = city != nil
if has_city then {
let lat = if city == "Berlin" then "52.52"
else if city == "Tokyo" then "35.68"
else if city == "New York" then "40.71"
else if city == "London" then "51.51"
else "52.52"
let lon = if city == "Berlin" then "13.41"
else if city == "Tokyo" then "139.76"
else if city == "New York" then "-74.01"
else if city == "London" then "-0.13"
else "13.41"
let url = "https://api.open-meteo.com/v1/forecast" +
"?latitude=" + lat +
"&longitude=" + lon +
"¤t_weather=true"
let response = perform Http.get(url)
// Check: did the HTTP request itself fail?
let got_response = response != nil
if got_response then {
let parsed = parse(response)
// Check: did the API return an error field?
let error_msg = get_string(parsed, "error", "")
let has_error = error_msg != ""
if has_error then {
perform IO.print("API error: " + error_msg)
} else {
// Check: does the response have current_weather?
let cw_str = get_string(parsed, "current_weather", "")
let has_weather = cw_str != ""
if has_weather then {
perform IO.print("Weather data received successfully!")
} else {
perform IO.print("No current weather in response.")
perform IO.print("Raw: " + stringify(parsed))
}
}
} else {
perform IO.print("Network error: could not reach the API.")
perform IO.print("Check your internet connection.")
}
} else {
perform IO.print("Usage: nulang weather.nula <city>")
}
Layered error handling:
response != nil— did the HTTP call succeed?get_string(parsed, "error", "")— did the API return an error?get_string(parsed, "current_weather", "")— is the expected data present?
get_string(json, key, default) returns the string value at key, or the
default if the field doesn't exist or isn't a string.
Use FS.write to persist output and stdlib::datetime to timestamp it:
import stdlib::json
import stdlib::datetime
let city = perform System.arg(2)
let has_city = city != nil
if has_city then {
let lat = if city == "Berlin" then "52.52"
else if city == "Tokyo" then "35.68"
else if city == "New York" then "40.71"
else if city == "London" then "51.51"
else "52.52"
let lon = if city == "Berlin" then "13.41"
else if city == "Tokyo" then "139.76"
else if city == "New York" then "-74.01"
else if city == "London" then "-0.13"
else "13.41"
let url = "https://api.open-meteo.com/v1/forecast" +
"?latitude=" + lat +
"&longitude=" + lon +
"¤t_weather=true"
let response = perform Http.get(url)
let ok = response != nil
if ok then {
// Capture the current time
let now = now()
let ts = perform Int.to_string(now.year) + "-" +
perform Int.to_string(now.month) + "-" +
perform Int.to_string(now.day) + " " +
perform Int.to_string(now.hour) + ":" +
perform Int.to_string(now.minute) + ":" +
perform Int.to_string(now.second)
perform IO.print("Fetched at: " + ts)
// Build a report and write it to disk
let report = "Weather Report — " + city + "\n" +
"Fetched: " + ts + "\n" +
"Raw JSON:\n" + response + "\n"
let out_path = "weather_report.txt"
perform FS.write(out_path, report)
perform IO.print("Report saved to " + out_path)
} else {
perform IO.print("Error: could not reach the weather API.")
}
} else {
perform IO.print("Usage: nulang weather.nula <city>")
}
Run it, then check the file:
nulang weather.nula Berlin
# → Fetched at: 2024-7-31 14:30:5
# → Report saved to weather_report.txt
cat weather_report.txt
# → Weather Report — Berlin
# → Fetched: 2024-7-31 14:30:5
# → Raw JSON:
# → {"latitude":52.52,...}import stdlib::datetime gives you now() — a DateTime record with
.year, .month, .day, .hour, .minute, .second fields, all Int.
FS.write(path, content) writes a string to disk. FS.read(path)
reads it back. Both are built-in effects — perform required.
Here's the complete weather CLI. Replace weather.nula with this:
import stdlib::json
import stdlib::datetime
// ── City coordinates lookup ──────────────────────────────────────────────
let get_coords = fn(city: String) {
if city == "Berlin" then { lat: "52.52", lon: "13.41" }
else if city == "Tokyo" then { lat: "35.68", lon: "139.76" }
else if city == "New York" then { lat: "40.71", lon: "-74.01" }
else if city == "London" then { lat: "51.51", lon: "-0.13" }
else if city == "Paris" then { lat: "48.85", lon: "2.35" }
else if city == "Sydney" then { lat: "-33.87", lon: "151.21" }
else if city == "São Paulo" then { lat: "-23.55", lon: "-46.63" }
else { lat: "52.52", lon: "13.41" } // default: Berlin
}
// ── Weather code → description ───────────────────────────────────────────
let describe_code = fn(code: Int) {
if code == 0 then "Clear sky"
else if code <= 3 then "Partly cloudy"
else if code <= 48 then "Fog"
else if code <= 55 then "Drizzle"
else if code <= 65 then "Rain"
else if code <= 75 then "Snow"
else if code <= 82 then "Rain showers"
else if code <= 99 then "Thunderstorm"
else "Unknown"
}
// ── Format timestamp ─────────────────────────────────────────────────────
let format_time = fn(dt) {
let pad = fn(n: Int) {
if n < 10 then "0" + perform Int.to_string(n)
else perform Int.to_string(n)
}
perform Int.to_string(dt.year) + "-" +
pad(dt.month) + "-" +
pad(dt.day) + " " +
pad(dt.hour) + ":" +
pad(dt.minute) + ":" +
pad(dt.second)
}
// ── Weather record type ──────────────────────────────────────────────────
let make_weather = fn(city: String, temp: Float, wind: Float, code: Int, desc: String) {
{ city: city, temp: temp, wind: wind, weather_code: code, description: desc }
}
// ── Main program ─────────────────────────────────────────────────────────
// 1. Read the city from the command line
let city = perform System.arg(2)
let has_city = city != nil
if has_city then {
// 2. Look up coordinates
let coords = get_coords(city)
// 3. Build the API URL
let url = "https://api.open-meteo.com/v1/forecast" +
"?latitude=" + coords.lat +
"&longitude=" + coords.lon +
"¤t_weather=true"
// 4. Check for optional API key
let api_key = perform Env.get("WEATHER_API_KEY")
let has_key = api_key != nil
perform IO.print("")
perform IO.print("═══════════════════════════════════")
perform IO.print(" Nulang Weather CLI")
perform IO.print("═══════════════════════════════════")
perform IO.print("City: " + city)
if has_key then {
perform IO.print("API key: " + api_key)
} else {
perform IO.print("API: Open-Meteo (free tier)")
}
perform IO.print("Fetching...")
// 5. Make the HTTP request
let response = perform Http.get(url)
let ok = response != nil
if ok then {
let size = perform String.length(response)
perform IO.print("Received " + perform Int.to_string(size) + " bytes")
// 6. Parse the JSON
let parsed = parse(response)
// 7. Check for API errors
let error_msg = get_string(parsed, "error", "")
let has_error = error_msg != ""
if has_error then {
perform IO.print("API error: " + error_msg)
} else {
// 8. Extract weather data
// The current_weather field is a nested JSON object.
// get_string on it returns the stringified inner object,
// which we re-parse to access temperature, windspeed, etc.
let cw_str = get_string(parsed, "current_weather", "")
let has_cw = cw_str != ""
if has_cw then {
let cw = parse(cw_str)
let temp = get_number(cw, "temperature", 0.0)
let wind = get_number(cw, "windspeed", 0.0)
let code = get_number(cw, "weathercode", 0.0)
let code_int = perform Float.to_int(code)
let desc = describe_code(code_int)
let weather = make_weather(city, temp, wind, code_int, desc)
// 9. Display the report
let report =
"────────────────────────────────\n" +
" Weather for " + weather.city + "\n" +
"────────────────────────────────\n" +
" Temperature: " + perform Float.to_string(weather.temp) + " °C\n" +
" Wind speed: " + perform Float.to_string(weather.wind) + " km/h\n" +
" Conditions: " + weather.description + "\n" +
"────────────────────────────────"
perform IO.print(report)
// 10. Save to file
let now = now()
let ts = format_time(now)
let file_report =
"Weather Report\n" +
"==============\n" +
"City: " + weather.city + "\n" +
"Fetched at: " + ts + "\n" +
"Temperature: " + perform Float.to_string(weather.temp) + " °C\n" +
"Wind speed: " + perform Float.to_string(weather.wind) + " km/h\n" +
"Conditions: " + weather.description + "\n" +
"\n" +
"Raw response:\n" +
stringify(parsed) + "\n"
let out_path = "weather_report.txt"
perform FS.write(out_path, file_report)
perform IO.print("")
perform IO.print("Report saved to " + out_path)
} else {
perform IO.print("No current_weather field in response.")
perform IO.print("Response: " + stringify(parsed))
}
}
} else {
perform IO.print("Error: could not reach the weather API.")
perform IO.print("Check your internet connection.")
}
} else {
perform IO.print("Usage: nulang weather.nula <city>")
perform IO.print("")
perform IO.print("Supported cities: Berlin, Tokyo, New York, London, Paris, Sydney, São Paulo")
perform IO.print("")
perform IO.print("Example:")
perform IO.print(" nulang weather.nula Tokyo")
}
Run it:
nulang weather.nula TokyoExpected output:
═══════════════════════════════════
Nulang Weather CLI
═══════════════════════════════════
City: Tokyo
API: Open-Meteo (free tier)
Fetching...
Received 225 bytes
────────────────────────────────
Weather for Tokyo
────────────────────────────────
Temperature: 21.3 °C
Wind speed: 15.8 km/h
Conditions: Partly cloudy
────────────────────────────────
Report saved to weather_report.txt
Congratulations! You've built a real Nulang application. Here's what you used:
| Concept | Where |
|---|---|
Effects (perform) |
IO.print, Http.get, FS.write, System.arg, Env.get |
Immutable bindings (let) |
Variables and functions throughout |
| Functions as values | make_weather, describe_code, get_coords, closures |
| Records | { lat: ..., lon: ... }, Weather record |
| If / then / else | Conditionals and city lookup |
!= nil / == nil |
Nil checks for error handling |
String concatenation (+) |
URL building, report formatting |
| HTTP requests | Http.get(url) |
| JSON parsing | parse(), stringify(), get_string(), get_number() |
| Pattern matching | (used implicitly via get_string/get_number) |
| Numeric conversion | Int.to_string, Float.to_string, Float.to_int |
| Date & time | now(), DateTime record fields |
| File I/O | FS.write(path, content) |
| Module imports | import stdlib::json, import stdlib::datetime |
Try each supported city:
nulang weather.nula "New York"
nulang weather.nula London
nulang weather.nula Paris
nulang weather.nula Sydney
nulang weather.nula "São Paulo"Try an unsupported city — it defaults to Berlin:
nulang weather.nula MumbaiObserve that:
- Different cities return different temperatures and wind speeds.
- The report file (
weather_report.txt) is overwritten each run. - The timestamp updates on every invocation.
Let's add --json and --verbose flags. Update the end of your program
(after the coordinate lookup and before the API call) to read a second
argument:
// Check for optional flags after the city name
let flag = perform System.arg(3)
let is_json = flag == "--json"
let is_verbose = flag == "--verbose"
Then wrap your output logic:
if is_json then {
// --json: print raw parsed JSON
perform IO.print(stringify(parsed))
} else {
// Default: human-readable report (the existing code)
...
}
if is_verbose then {
// --verbose: print extra details
perform IO.print("URL: " + url)
perform IO.print("Response: " + perform Int.to_string(size) + " bytes")
perform IO.print("Lat/Lon: " + coords.lat + ", " + coords.lon)
}
Run with flags:
nulang weather.nula Berlin --json
nulang weather.nula Tokyo --verbose
nulang weather.nula London --json --verboseThis demonstrates combining positional and flag-style arguments. Add the
flag logic right after reading the city name and before the if has_city
block for clean integration.
// ❌ WRONG — missing perform
IO.print("hello")
Http.get(url)
Int.to_string(n)
// ✅ CORRECT — every built-in effect needs perform
perform IO.print("hello")
perform Http.get(url)
perform Int.to_string(n)
Why: Built-in effects (IO, Http, FS, System, Env, Int,
String, Float, Array) are VM-wired and require the perform keyword.
Without it, the compiler treats them as unknown functions.
// ❌ WRONG
import stdlib.json
import stdlib.datetime
// ✅ CORRECT
import stdlib::json
import stdlib::datetime
Why: Module paths use :: as the separator. Imported names are
unqualified — after import stdlib::json, call parse(...) directly
(not json.parse(...)).
let response = perform Http.get(url)
// ❌ WRONG — checks for failure but treats it as success
if response == nil then {
perform IO.print("Got data!") // this runs when there's NO data!
}
// ✅ CORRECT
if response != nil then {
perform IO.print("Got data!") // this runs when data IS present
}
Why: nil means "no value." response != nil means "the request
succeeded and we have data." response == nil means "the request failed."
Double-check your logic — it's easy to invert by accident.
Make sure you're using nulang (not nula directly) and that your file
path is correct:
nulang weather.nula Berlin # ✅ correct
nulang nula run # ✅ if using a package with Nulang.tomlIf you scaffolded with nulang nula new, run nulang nula run from the
project directory. Arguments may pass differently through the package
manager — check your Nulang.toml configuration.
docs/GETTING_STARTED.md— deeper dive into effects, actors, pattern matchingdocs/PITFALLS.md— common mistakes and idiomatic fixesexamples/— 17 verified example programs covering every language featureexamples/16_realworld.nula— the JSON fetcher that inspired this tutorialexamples/17_actor_fetcher.nula— multi-URL fetcher with detailed reportingnulang --repl— interactive exploration with:help,:type,:load
Built with Nulang — an actor-based language with algebraic effects and capability tracking.