ballerina langを触る hello world編

はじめに

同僚から聞いたballerinaという言語を触ってみる。インドのほうで流行っているらしい。

ソースコードをシーケンス図やフローチャートとしてグラフィカルに表示と編集できるのは面白そうなので触ってみることにした。

言語の特徴

インストール方法

こちらを参照してください。
Ballerina downloads - The Ballerina programming language

私はhome brewを使っているので以下コマンドでインストール

1
$ brew install bal

本記事は以下のバージョンを使ったものです。

1234
$ bal version
Ballerina 2201.13.1 (Swan Lake Update 13)
Language specification 2024R1
Update Tool 1.5.1

プロジェクト作成

先ずはGet startedの内容をやってみよう
https://ballerina.io/learn/get-started/

以下コマンドでプロジェクトを作成できるそうだ。

1
$ bal new hoge

試しに、hello-worldをするためだけのプジェクトを作成するとこんな感じ。
はじめから.devcontainer.jsonが入っているのが今風だ。

1234567891011
$ bal new etude_bal_hello_world
Created new package 'etude_bal_hello_world' at etude_bal_hello_world.
$ cd etude_bal_hello_world/
$ ls -al
total 32
drwxr-xr-x@  6 wagomu  staff  192 Jan  3 10:38 ./
drwxr-xr-x@ 13 wagomu  staff  416 Jan  3 10:38 ../
-rw-r--r--@  1 wagomu  staff  148 Jan  3 10:38 .devcontainer.json
-rw-r--r--@  1 wagomu  staff  483 Jan  3 10:38 .gitignore
-rw-r--r--@  1 wagomu  staff  147 Jan  3 10:38 Ballerina.toml
-rw-r--r--@  1 wagomu  staff   82 Jan  3 10:38 main.bal

各種ファイルはこのような感じだ。
ドキュメントを見るとBallerina.tomlはパッケージのルートを識別するためにも使用されているようだ。

123456789
$ cat Ballerina.toml
[package]
org = "wagomu"
name = "etude_bal_hello_world"
version = "0.1.0"
distribution = "2201.13.1"

[build-options]
observabilityIncluded = true
123456
$ cat main.bal
import ballerina/io;

public function main() {
    io:println("Hello, World!");
}
123456789
$ cat .devcontainer.json
{
  "image": "ballerina/ballerina-devcontainer:2201.10.3",
  "customizations": {
    "vscode": {
      "extensions": ["WSO2.ballerina"]
    }
  }
}
123456789101112
$ cat .gitignore
# Ballerina generates this directory during the compilation of a package.
# It contains compiler-generated artifacts and the final executable if this is an application package.
target/

# Ballerina maintains the compiler-generated source code here.
# Remove this if you want to commit generated sources.
generated/

# Contains configuration values used during development time.
# See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details.
Config.toml

注意

プロジェクト名を-で作成したところ、_区切りにするほうが良さそうだった

1234
$ bal new etude-bal-hello-world
Package name is derived as 'etude_bal_hello_world'. Edit the Ballerina.toml to change it.

Created new package 'etude_bal_hello_world' at etude-bal-hello-world.

hello worldを実行してみる

bal runで実行できる。

123456789
$ bal run
Compiling source
        wagomu/etude_bal_hello_world:0.1.0
ballerina/io:1.6.3 [central.ballerina.io ->/Users/wagomu/.ballerina/repositories/central.ballerina.io/bala/ballerina/io/1.6.3]  100% [======================================================================] 112/112 KB (0:00:00 / 0:00:00)
        ballerina/io:1.6.3 pulled from central successfully

Running executable

Hello, World!

実行後のファイル構成は以下のようになった。
新たにDependencies.tomltarget/が作成された。

12345678910
$ ls -al
total 40
drwxr-xr-x@  8 wagomu  staff   256 Jan  3 10:55 ./
drwxr-xr-x@ 13 wagomu  staff   416 Jan  3 10:38 ../
-rw-r--r--@  1 wagomu  staff   148 Jan  3 10:38 .devcontainer.json
-rw-r--r--@  1 wagomu  staff   483 Jan  3 10:38 .gitignore
-rw-r--r--@  1 wagomu  staff   147 Jan  3 10:38 Ballerina.toml
-rw-r--r--@  1 wagomu  staff  1357 Jan  3 10:56 Dependencies.toml
-rw-r--r--@  1 wagomu  staff    82 Jan  3 10:55 main.bal
drwxr-xr-x@  6 wagomu  staff   192 Jan  3 10:56 target/

内容はこのような感じ。
依存関係を表すtomlファイルとjarファイルだ。
java21を使っているようだ。

123456789101112131415161718192021
$ cat Dependencies.toml | head -20
# AUTO-GENERATED FILE. DO NOT MODIFY.

# This file is auto-generated by Ballerina for managing dependency versions.
# It should not be modified by hand.

[ballerina]
dependencies-toml-version = "2"
distribution-version = "2201.13.1"

[[package]]
org = "ballerina"
name = "io"
version = "1.8.0"
dependencies = [
        {org = "ballerina", name = "jballerina.java"},
        {org = "ballerina", name = "lang.value"}
]
modules = [
        {org = "ballerina", packageName = "io", moduleName = "io"}
]
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
$ ls -alR target/
total 8
drwxr-xr-x@ 7 wagomu  staff   224 Jan  3 12:19 ./
drwxr-xr-x@ 8 wagomu  staff   256 Jan  3 12:19 ../
drwxr-xr-x@ 3 wagomu  staff    96 Jan  3 12:19 backup/
drwxr-xr-x@ 3 wagomu  staff    96 Jan  3 12:20 bin/
-rw-r--r--@ 1 wagomu  staff  1869 Jan  3 12:19 build
drwxr-xr-x@ 3 wagomu  staff    96 Jan  3 12:19 cache/
drwxr-xr-x@ 2 wagomu  staff    64 Jan  3 12:19 resources/

target/backup:
total 0
drwxr-xr-x@ 3 wagomu  staff   96 Jan  3 12:19 ./
drwxr-xr-x@ 7 wagomu  staff  224 Jan  3 12:19 ../
drwxr-xr-x@ 3 wagomu  staff   96 Jan  3 12:19 bin/

target/backup/bin:
total 21344
drwxr-xr-x@ 3 wagomu  staff        96 Jan  3 12:19 ./
drwxr-xr-x@ 3 wagomu  staff        96 Jan  3 12:19 ../
-rw-r--r--@ 1 wagomu  staff  10925925 Jan  3 12:19 etude_bal_hello_world.jar

target/bin:
total 21344
drwxr-xr-x@ 3 wagomu  staff        96 Jan  3 12:20 ./
drwxr-xr-x@ 7 wagomu  staff       224 Jan  3 12:19 ../
-rw-r--r--@ 1 wagomu  staff  10925925 Jan  3 12:20 etude_bal_hello_world.jar

target/cache:
total 0
drwxr-xr-x@ 3 wagomu  staff   96 Jan  3 12:19 ./
drwxr-xr-x@ 7 wagomu  staff  224 Jan  3 12:19 ../
drwxr-xr-x@ 3 wagomu  staff   96 Jan  3 12:19 wagomu/

target/cache/wagomu:
total 0
drwxr-xr-x@ 3 wagomu  staff  96 Jan  3 12:19 ./
drwxr-xr-x@ 3 wagomu  staff  96 Jan  3 12:19 ../
drwxr-xr-x@ 3 wagomu  staff  96 Jan  3 12:19 etude_bal_hello_world/

target/cache/wagomu/etude_bal_hello_world:
total 0
drwxr-xr-x@ 3 wagomu  staff   96 Jan  3 12:19 ./
drwxr-xr-x@ 3 wagomu  staff   96 Jan  3 12:19 ../
drwxr-xr-x@ 4 wagomu  staff  128 Jan  3 12:19 0.1.0/

target/cache/wagomu/etude_bal_hello_world/0.1.0:
total 0
drwxr-xr-x@ 4 wagomu  staff  128 Jan  3 12:19 ./
drwxr-xr-x@ 3 wagomu  staff   96 Jan  3 12:19 ../
drwxr-xr-x@ 3 wagomu  staff   96 Jan  3 12:19 bir/
drwxr-xr-x@ 3 wagomu  staff   96 Jan  3 12:19 java21/

target/cache/wagomu/etude_bal_hello_world/0.1.0/bir:
total 8
drwxr-xr-x@ 3 wagomu  staff    96 Jan  3 12:19 ./
drwxr-xr-x@ 4 wagomu  staff   128 Jan  3 12:19 ../
-rw-r--r--@ 1 wagomu  staff  3333 Jan  3 12:19 etude_bal_hello_world.bir

target/cache/wagomu/etude_bal_hello_world/0.1.0/java21:
total 56
drwxr-xr-x@ 3 wagomu  staff     96 Jan  3 12:19 ./
drwxr-xr-x@ 4 wagomu  staff    128 Jan  3 12:19 ../
-rw-r--r--@ 1 wagomu  staff  25124 Jan  3 12:19 wagomu-etude_bal_hello_world-0.1.0.jar

target/resources:
total 0
drwxr-xr-x@ 2 wagomu  staff   64 Jan  3 12:19 ./
drwxr-xr-x@ 7 wagomu  staff  224 Jan  3 12:19 ../

jarファイルの生成のみをしたい場合には、bal buildというコマンドがある。

123456
$ bal build
Compiling source
        wagomu/etude_bal_hello_world:0.1.0

Generating executable
        target/bin/etude_bal_hello_world.jar

生成されたjarファイルを実行するためにもbal runコマンドを利用するようだ。

12
$ bal run target/bin/etude_bal_hello_world.jar
Hello, World!

実験1

試しにhoge.balmain.balを作成してbal runを実行したところ、以下エラーとなった。
main.balのmain関数を実行していると思ったが、ファイル名は関係なくmain関数を探しているっぽい。

123456789101112131415161718
$ cat hoge.bal
import ballerina/io;                                                                                                                                                                                                                           ⏎
public function main() {
    io:println("hoge");
}

$ cat main.bal
import ballerina/io;

public function main() {
    io:println("Hello, World!");
}

> bal run
Compiling source
        wagomu/etude_bal_hello_world:0.1.0
ERROR [main.bal:(3:17,3:21)] redeclared symbol 'main'
error: compilation contains errors

実験2

実験1で結果は分かっているが念の為の実験。main.balを削除して、hoge.balのみの状態でbal runをしたところ、実行できた。やはりファイル名は関係なくmain関数を探して実行しているようだ。

1234567891011121314151617181920212223
$ ls -al
total 32
drwxr-xr-x@  6 wagomu  staff  192 Jan  3 10:53 ./
drwxr-xr-x@ 13 wagomu  staff  416 Jan  3 10:38 ../
-rw-r--r--@  1 wagomu  staff  148 Jan  3 10:38 .devcontainer.json
-rw-r--r--@  1 wagomu  staff  483 Jan  3 10:38 .gitignore
-rw-r--r--@  1 wagomu  staff  147 Jan  3 10:38 Ballerina.toml
-rw-r--r--@  1 wagomu  staff   73 Jan  3 10:51 hoge.bal

$ cat hoge.bal
import ballerina/io;

public function main() {
    io:println("hoge");
}

$ bal run
Compiling source
        wagomu/etude_bal_hello_world:0.1.0

Running executable

hoge

REST APIを作る

main.balを以下のように書き換えて実行し、別のターミナルからcurlで呼び出したところ以下のようになった。
service / on new http:Listener(8080) { ... }と、
Country[] countries = check countriesClient->/countries;という書き方が独特だなぁ。

123456789101112131415161718192021222324252627
$ cat main.bal
import ballerina/http;

public type Country record {
    string name;
    string continent;
    int population;
    decimal gdp;
    decimal area;
};

final http:Client countriesClient = check new ("https://dev-tools.wso2.com/gs/helpers/v1.0/");

service / on new http:Listener(8080) {

    resource function get countries() returns Country[]|error {
        // Sending a GET request to the "/countries" endpoint and retrieving an array of `Country` records.
        Country[] countries = check countriesClient->/countries;
        return countries;
    }
}

$ bal run
Compiling source
        wagomu/etude_bal_hello_world:0.1.0

Running executable
12
$ curl localhost:8080/countries
[{"name":"United States", "continent":"North America", "population":331002651, "gdp":2.736E+13, "area":9833517.0, "capital":"Washington, D.C.", "languages":"English", "currency":"USD"}, {"name":"Canada", "continent":"North America", "population":37742154, "gdp":2.14E+12, "area":9984670.0, "capital":"Ottawa", "languages":"English, French", "currency":"CAD"}, {"name":"Brazil", "continent":"South America", "population":212559417, "gdp":1.9444E+12, "area":8515767.0, "capital":"Brasília", "languages":"Portuguese", "currency":"BRL"}, {"name":"United Kingdom", "continent":"Europe", "population":67886011, "gdp":2.56E+12, "area":243610.0, "capital":"London", "languages":"English", "currency":"GBP"}, {"name":"Germany", "continent":"Europe", "population":83783942, "gdp":4.5E+12, "area":357022.0, "capital":"Berlin", "languages":"German", "currency":"EUR"}, {"name":"France", "continent":"Europe", "population":65273511, "gdp":2.93E+12, "area":551695.0, "capital":"Paris", "languages":"French", "currency":"EUR"}, {"name":"India", "continent":"Asia", "population":1380004385, "gdp":3.2E+12, "area":3287263.0, "capital":"New Delhi", "languages":"Hindi, English", "currency":"INR"}, {"name":"China", "continent":"Asia", "population":1439323776, "gdp":1.773E+13, "area":9596961.0, "capital":"Beijing", "languages":"Mandarin", "currency":"CNY"}, {"name":"Japan", "continent":"Asia", "population":126476461, "gdp":4.2E+12, "area":377975.0, "capital":"Tokyo", "languages":"Japanese", "currency":"JPY"}, {"name":"Australia", "continent":"Oceania", "population":25499884, "gdp":1.78E+12, "area":7692024.0, "capital":"Canberra", "languages":"English", "currency":"AUD"}, {"name":"South Africa", "continent":"Africa", "population":59308690, "gdp":1.275E+12, "area":1219090.0, "capital":"Pretoria", "languages":"11 official languages", "currency":"ZAR"}, {"name":"Russia", "continent":"Europe/Asia", "population":145934462, "gdp":2.196E+12, "area":17098242, "capital":"Moscow", "languages":"Russian", "currency":"RUB"}, {"name":"Mexico", "continent":"North America", "population":128932753, "gdp":1.79E+12, "area":1964375.0, "capital":"Mexico City", "languages":"Spanish", "currency":"MXN"}, {"name":"Italy", "continent":"Europe", "population":60461826, "gdp":2.0E+12, "area":301340.0, "capital":"Rome", "languages":"Italian", "currency":"EUR"}, {"name":"Argentina", "continent":"South America", "population":45195774, "gdp":4.07E+11, "area":2780400.0, "capital":"Buenos Aires", "languages":"Spanish", "currency":"ARS"}, {"name":"Spain", "continent":"Europe", "population":46754778, "gdp":1.4E+12, "area":505990.0, "capital":"Madrid", "languages":"Spanish", "currency":"EUR"}, {"name":"Indonesia", "continent":"Asia", "population":273523615, "gdp":1.119E+12, "area":1904569.0, "capital":"Jakarta", "languages":"Indonesian", "currency":"IDR"}, {"name":"Saudi Arabia", "continent":"Asia", "population":34813871, "gdp":7.93E+11, "area":2149690.0, "capital":"Riyadh", "languages":"Arabic", "currency":"SAR"}, {"name":"South Korea", "continent":"Asia", "population":51269185, "gdp":1.647E+12, "area":100210.0, "capital":"Seoul", "languages":"Korean", "currency":"KRW"}, {"name":"Turkey", "continent":"Europe/Asia", "population":84339067, "gdp":7.2E+11, "area":783562.0, "capital":"Ankara", "languages":"Turkish", "currency":"TRY"}, {"name":"Egypt", "continent":"Africa", "population":102334404, "gdp":3.63E+11, "area":1002450.0, "capital":"Cairo", "languages":"Arabic", "currency":"EGP"}, {"name":"Thailand", "continent":"Asia", "population":69799978, "gdp":5.43E+11, "area":513120.0, "capital":"Bangkok", "languages":"Thai", "currency":"THB"}, {"name":"Pakistan", "continent":"Asia", "population":220892340, "gdp":2.78E+11, "area":881913.0, "capital":"Islamabad", "languages":"Urdu, English", "currency":"PKR"}, {"name":"Nigeria", "continent":"Africa", "population":206139589, "gdp":4.32E+11, "area":923768.0, "capital":"Abuja", "languages":"English", "currency":"NGN"}, {"name":"Vietnam", "continent":"Asia", "population":97338579, "gdp":3.4E+11, "area":331212.0, "capital":"Hanoi", "languages":"Vietnamese", "currency":"VND"}, {"name":"Philippines", "continent":"Asia", "population":109581078, "gdp":4.02E+11, "area":300000.0, "capital":"Manila", "languages":"Filipino, English", "currency":"PHP"}, {"name":"Colombia", "continent":"South America", "population":50882891, "gdp":3.24E+11, "area":1141748.0, "capital":"Bogotá", "languages":"Spanish", "currency":"COP"}, {"name":"Sri Lanka", "continent":"Asia", "population":21803000, "gdp":8.41E+10, "area":65610.0, "capital":"Sri Jayawardenepura Kotte", "languages":"Sinhala, Tamil", "currency":"LKR"}]

さらに改善して、レスポンスの内容を変更させるとこのような感じだ。
C#で見慣れたLINQのような構文が採用されている。
個人的に面白いなと感じたのが、LINQのような構文の途中で変数宣言を行ない、その変数をもとにorder byをしている点だ。

12
            let decimal gdpPerCapita = (gdp / population).round(2) // Calculating and rounding GDP per capita to 2 decimal places.
            order by gdpPerCapita descending // Sorting the results by GDP per capita in descending order.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
$ cat main.bal
import ballerina/http;

public type Country record {
    string name;
    string continent;
    int population;
    decimal gdp;
    decimal area;
};

public type CountryResponse record {
    string name;
    string continent;
    decimal gdpPerCapita;
};

final http:Client countriesClient = check new ("https://dev-tools.wso2.com/gs/helpers/v1.0/");

service / on new http:Listener(8080) {

    resource function get countries() returns CountryResponse[]|http:InternalServerError {
        do {
            // Sending a GET request to the "/countries" endpoint and retrieving an array of `Country` records.
            Country[] countries = check countriesClient->/countries;

            // Using a query expression to process the list of countries and generate a summary.
            CountryResponse[] topCountries =
                from var {name, continent, population, area, gdp} in countries
            where population >= 100000000 && area >= 1000000d // Filtering countries with a population >= 100M and area >= 1M sq km.
            let decimal gdpPerCapita = (gdp / population).round(2) // Calculating and rounding GDP per capita to 2 decimal places.
            order by gdpPerCapita descending // Sorting the results by GDP per capita in descending order.
            limit 10 // Limiting the results to the top 10 countries.
            select {name, continent, gdpPerCapita}; // Selecting the country name, continent, and GDP per capita.
            return topCountries;
        } on fail var err {
            return <http:InternalServerError>{
                body: {
                    "error": "Failed to retrieve countries",
                    "message": err.message()
                }
            };
        }
    }
}

$ bal run
Compiling source
        wagomu/etude_bal_hello_world:0.1.0

Running executable
123456
$ curl localhost:8080/countries
[{"name":"United States", "continent":"North America", "gdpPerCapita":82657.95}, {"name":"Russia", "continent":"Europe/Asia", "gdpPerCapita":15047.85}, {"name":"Mexico", "continent":"North America", "gdpPerCapita":13883.21}, {"name":"China", "continent":"Asia", "gdpPerCapita":12318.28}, {"name":"Brazil", "continent":"South America", "gdpPerCapita":9147.56}, {"name":"Indonesia", "continent":"Asia", "gdpPerCapita":4091.05}, {"name":"Egypt", "continent":"Africa", "gdpPerCapita":3547.19}, {"name":"India", "continent":"Asia", "gdpPerCapita":2318.83}]

# 存在しないリクエストを実行した場合
$ curl localhost:8080/hoge
{"timestamp":"2026-01-03T02:26:14.159298Z", "status":404, "reason":"Not Found", "message":"no matching resource found for path : /hoge , method : GET", "path":"/hoge", "method":"GET"}⏎                                           

おわりに

ソースコードのビジュアライズは次回に回そうと思います。