現代のアプリはかつての単純なWebサイトとは求められるものが違うのに、プラットフォームは相変わらずブラウザーのままで、Webアプリの核となる言語はJavaScriptです。
JavaScriptはすべての場面で最適なわけではなく、複雑なアプリでは特に弱点があります。回避するために生まれたのが、新しい言語とそれに合わせたコンパイラーです。JavaScriptのコードを書かずにブラウザー上で動作するコードが書け、JavaScriptの制約に縛られることもありません。
本記事では、JavaScriptへのコンパイルによってブラウザーやNode.jsなどのプラットフォーム上で動作する優れた言語を10種類紹介します。
Dart
Dartは古典的なオブジェクト指向言語です。すべてがオブジェクトで、オブジェクトはいずれかのクラスのインスタンスです(関数としても機能します)。この言語はブラウザー、サーバー、モバイルデバイス上で動くアプリに特化して作られました。グーグルが管理しており、グーグルの収益上もっとも重要な次世代のAdWords UIで使われている言語でもあります。大規模アプリにも対応できる強力な言語であることが分かります。
Dartはブラウザー上で動作するJavaScriptに変換コンパイルでき、サーバーアプリ構築用のDart VM(バーチャルマシン)を使えば、そのまま実装することも可能です。モバイルアプリ開発にはFlutter SDKを使います。
複雑なアプリ開発では熟成したライブラリーやあるタスクに特化した機能を使いたいところです。Dartはすべてを持ち合わせています。代表的なライブラリーはDart用のAngular「AngularDart」です。
型を明示して型安全性(type-safe)を持つコードが書けるうえ、推測できる型は指定しなくてもいいので煩わしくありません。詳細で悩まずに高速プロトタイピングして、一度うまくいったら型を追加することでコードを堅牢にできます。
VM上での並行処理は、共有メモリースレッドの代わりに(Dartは単一スレッド)、DartではIsolatesと呼ばれる単位でメモリーヒープを分け、各Isolates間のやりとりにはメッセージを用います。ブラウザーの場合は少し異なり、isolatesではなくWorkersを新規作成します。
// Example extracted from dartlang.org
import 'dart:async';
import 'dart:math' show Random;
main() async {
print('Compute π using the Monte Carlo method.');
await for (var estimate in computePi()) {
print('π ≅ $estimate');
}
}
/// Generates a stream of increasingly accurate estimates of π.
Stream<double> computePi({int batch: 1000000}) async* {
var total = 0;
var count = 0;
while (true) {
var points = generateRandom().take(batch);
var inside = points.where((p) => p.isInsideUnitCircle);
total += batch;
count += inside.length;
var ratio = count / total;
// Area of a circle is A = π⋅r², therefore π = A/r².
// So, when given random points with x ∈ <0,1>,
// y ∈ <0,1>, the ratio of those inside a unit circle
// should approach π / 4. Therefore, the value of π
// should be:
yield ratio * 4;
}
}
Iterable<Point> generateRandom([int seed]) sync* {
final random = new Random(seed);
while (true) {
yield new Point(random.nextDouble(), random.nextDouble());
}
}
class Point {
final double x, y;
const Point(this.x, this.y);
bool get isInsideUnitCircle => x * x + y * y <= 1;
}
TypeScript
TypeScriptはJavaScriptの上位互換(スーパーセット)です。static修飾子が必要ですが、JavaScriptのコードはTypeScript上でも有効です。TypeScriptのコンパイラーはES2015以降に相当する機能への変換コンパイルにも対応しているため、常に最新の機能が使えます。
ほかの言語とは異なりTypeScriptはJavaScriptの思想を受け継ぎ、コードの健全性を改善する特徴だけ追加されています。型アノテーションをはじめ型に関する機能があり、静的コード解析やそのほかのリファクタリングに便利なツールが使えるため、「より快適に書けるJavaScript」に仕上がっています。型があることで、別々のコンポーネント間のデータの受け渡しも改善しています。
型推論に対応していて、毎回すべての型を書かなくても大丈夫です。手早く動くものを作り、あとで型を追加してコードを強化することが可能です。
TypeScriptは、交差型(intersection types)、共用体型(union types)、型のエイリアス(type aliases)、判別共用体型(descriminated unions)、型の保護(type guards)などの高度な型を持っています。詳しくはTypeScript公式ドキュメントの高度な型を見てください。
Reactを使っている場合は、Reactの型を追加することでJSXも使えます。
class Person {
private name: string;
private age: number;
private salary: number;
constructor(name: string, age: number, salary: number) {
this.name = name;
this.age = age;
this.salary = salary;
}
toString(): string {
return `${this.name} (${this.age}) (${this.salary})`;
}
}
Elm
Elmは、JavaScript、HTML、CSSにコンパイルできる純粋な関数型言語です。Elmだけで完全なサイトが構築できるため、ReactなどのJavaScriptフレームワークの代わりにも使えます。Elmで作られたアプリは自動でバーチャルDOMライブラリーを利用するため非常に高速です。Elmの利点は、この設計でデータフローを気にせずにデータの宣言やロジックに集中できます。
Elmのすべての関数は純粋(同じ入力に対しては同じ出力が返る)です。意図的に作り替えないかぎり関数はそれ以外のことをしません。外部APIを利用したいなら、外部と通信するcommand関数を作り、応答を得るためにsubscriptions関数を作ります。純粋性の特徴は、値は変更されない(immutable)点です。必要なら、既存の値を変更するのではなく、あらためて別の値を生成します。
portsを使えばJavaScriptやほかのライブラリーとやりとりできるため、徐々に採用することも可能です。まだ正式なバージョン1に達していませんが、すでに複雑で大型のアプリケーションでも採用されていることから、複雑なアプリでも選択できます。
Elmの魅力は、中身の分からないメッセージではなく、コードの修正の助けになるコードを生成することで、言語を習得する際に、コンパイラー自体が大きな助けになります。初心者にもやさしいコンパイラーです。
module Main exposing (..)
import Html exposing (..)
-- MAIN
main : Program Never Model Msg
main =
Html.program
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
-- INIT
type alias Model = String
init : ( Model, Cmd Msg )
init = ( "Hello World!", Cmd.none )
-- UPDATE
type Msg
= DoNothing
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
DoNothing ->
( model, Cmd.none )
-- VIEW
view : Model -> Html Msg
view model =
div [] [text model]
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
PureScript
PureScriptはPhil Freemanが作成した、純粋な関数型で強力な型付けを持つプログラミング言語です。思想的にはHaskellと同様に既存のJavaScriptライブラリーとの高い互換性を持つように設計されつつも、核は依然JavaScriptです。
PureScriptの強みはコンパクトさです。ほかの言語では必須のライブラリーに依存しません。たとえばコンパイラーは、ジェネレーターやプロミスを読み込まず、タスクの処理に適したライブラリーを使えます。必要な機能に応じた実装が可能なので、生成されるコードを最小限に保ちつつ高効率で最適なアプリが作れます。
もう1つの特徴は、ライブラリーとツールを含むJavaScriptとの互換性を保ちながらもクリーンで可読性のあるコードを生成するコンパイラーです。
ほかの言語と同様、PureScriptには独自のビルドツール「Pulp」があります。Gulpに似ていますが、PureScriptで書かれたプロジェクト専用です。
型に関しては、別のメタ言語風のElmとは異なり、高階型変数や型クラスなどの高度な型に対応しています。Haskellの機能を使っていて、洗練された抽象化を可能にします。
module Main where
import Prelude
import Data.Foldable (fold)
import TryPureScript
main =
render $ fold
[ h1 (text "Try PureScript!")
, p (text "Try out the examples below, or create your own!")
, h2 (text "Examples")
, list (map fromExample examples)
]
where
fromExample { title, gist } =
link ("?gist=" <> gist) (text title)
examples =
[ { title: "Algebraic Data Types"
, gist: "37c3c97f47a43f20c548"
}
, { title: "Loops"
, gist: "cfdabdcd085d4ac3dc46"
}
, { title: "Operators"
, gist: "3044550f29a7c5d3d0d0"
}
]
CoffeeScript
CoffeeScriptはJavaScriptの良いところを極力活かしながらも、よりスッキリした構文と最適なセマンティック(意味付け)を実現します。人気は近年陰りが見られるものの路線を変更し、ES2015以上の機能を実装するメジャーアップデートもありました。
CoffeeScriptのコードは可読性のあるJavaScriptへ直接変換され、既存ライブラリーとの互換も保たれます。バージョン2からはコンパイラーは最新版のECMAScriptと互換のあるコードを生成するため、CoffeeScriptでclassを使うと、JavaScriptでもclassが生成されます。JSXがCoffeeScriptと互換性があるのは、Reactを使っている人には朗報でしょう。
コンパイラーの際立った特徴は、 文芸的プログラミング(Literate Programming)で書かれたコードを生成できる点です。コード内で強調を使ったりコメントを別途用意したりする代わりに、コメントありきで、コードは時折現れる程度です。プログラムソースファイルは技術ドキュメントのようになる、Donald Knuthが提唱したスタイルです。
ほかの言語と異なり、CoffeeScriptのコードはライブラリーを使い直接ブラウザーで解釈します。クイックテストを作りたければtext/coffeescriptタグ内にコードを書いてコンパイラーをincludeすれば、コードは直接JavaScriptとして解釈されます。
# Assignment:
number = 42
opposite = true
# Conditions:
number = -42 if opposite
# Functions:
square = (x) -> x * x
# Arrays:
list = [1, 2, 3, 4, 5]
# Objects:
math =
root: Math.sqrt
square: square
cube: (x) -> x * square x
# Splats:
race = (winner, runners...) ->
print winner, runners
# Existence:
alert "I knew it!" if elvis?
# Array comprehensions:
cubes = (math.cube num for num in list)
ClojureScript
ClojureScriptは、プログラミング言語のClojureからJavaScriptに変換するコンパイラーです。Clojureは動的型付けをもつ汎用の関数型言語で、変更不可な(immutable)データ構造に対応しています。
ClojureScriptは本リスト中で唯一Lisp系のプログラミング言語なので、近い特徴があります。コードはデータとして扱われ、マクロが使えるのでメタプログラミングが可能です。ほかのLisp系言語とは違いClojureは変更不可の(immutable)データ構造に対応しており、副作用の管理がしやすくなっています。
括弧の使い方をはじめ、はじめての人にとっては取っ付きにくく見える構文ですが、深い理由があり、長い目で見ると良さに気が付くでしょう。構文の簡潔さと抽象化能力により、高度な抽象化が必要な場面においてLispは強力なツールとなります。
Clojureは基本的には関数型言語ですが、PureScriptやElmのように純粋ではありません。そのため副作用(side-effect)の可能性は残っていますが、そのほかの関数型言語の特徴は有しています。
ClojureScriptはコードの最適化にGoogle Closureを使用しているため、既存のJavaScriptライブラリーと互換性があります。
; Extracted from https://github.com/clojure/clojurescript/blob/master/samples/dom/src/dom/test.cljs
(ns dom.test
(:require [clojure.browser.event :as event]
[clojure.browser.dom :as dom]))
(defn log [& args]
(.log js/console (apply pr-str args)))
(defn log-obj [obj]
(.log js/console obj))
(defn log-listener-count []
(log "listener count: " (event/total-listener-count)))
(def source (dom/get-element "source"))
(def destination (dom/get-element "destination"))
(dom/append source
(dom/element "Testing me ")
(dom/element "out!"))
(def success-count (atom 0))
(log-listener-count)
(event/listen source
:click
(fn [e]
(let [i (swap! success-count inc)
e (dom/element :li
{:id "testing"
:class "test me out please"}
"It worked!")]
(log-obj e)
(log i)
(dom/append destination
e))))
(log-obj (dom/element "Text node"))
(log-obj (dom/element :li))
(log-obj (dom/element :li {:class "foo"}))
(log-obj (dom/element :li {:class "bar"} "text node"))
(log-obj (dom/element [:ul [:li :li :li]]))
(log-obj (dom/element :ul [:li :li :li]))
(log-obj (dom/element :li {} [:ul {} [:li :li :li]]))
(log-obj (dom/element [:li {:class "baz"} [:li {:class "quux"}]]))
(log-obj source)
(log-listener-count)
Scala.js
Scala.jsは、プログラミング言語ScalaをJavaScriptに変換するコンパイラーです。Scalaは、採用しやすくて強力なツールとしてオブジェクト指向言語と関数型言語の特徴を1つの言語に盛り込む言語です。
厳密な型付けを持つ言語なので、部分的型推定による柔軟な型システムが使いやすいです。たいていの値は型推定できますが、関数パラメーターには明示的な型アノテーションが必須です。
一般的なオブジェクト指向のパターンの多くに対応していますが(例:すべての値はオブジェクトであり、その演算はメソッドのコールである)、関数型言語の特徴である第一級関数(first-class function)や変更不可(immutable)なデータ構造などの恩恵も受けられます。
Scala.jsは、使い慣れたオブジェクト指向から始めつつも、自分のペースと必要性に合わせて苦労なく関数型への移行ができる利点があります。既存のJavaScriptコードやライブラリーと、Scalaコードとの互換性もあります。
Scala初心者でも以下のコードを見比べれば、JavaScriptと極端な違いはないと分かるでしょう。
// JavaScript
var xhr = new XMLHttpRequest();
xhr.open("GET",
"https://api.twitter.com/1.1/search/" +
"tweets.json?q=%23scalajs"
);
xhr.onload = (e) => {
if (xhr.status === 200) {
var r = JSON.parse(xhr.responseText);
$("#tweets").html(parseTweets(r));
}
};
xhr.send();
// Scala.js
val xhr = new XMLHttpRequest()
xhr.open("GET",
"https://api.twitter.com/1.1/search/" +
"tweets.json?q=%23scalajs"
)
xhr.onload = { (e: Event) =>
if (xhr.status == 200) {
val r = JSON.parse(xhr.responseText)
$("#tweets").html(parseTweets(r))
}
}
xhr.send()
Reason
ReasonはFacebookにより開発・維持されている言語で、OCaml(オーキャメル)コンパイラー用に新しい構文を追加したもので、JavaScriptもしくはネイティブコードの両方に変換可能です。
関数型言語であるML言語の方言なので、強力かつ柔軟な型システムを持ち、型推論、代数的データ型、パターンマッチングなどを備えています。変更不可(immutable)なデータ型や多態ポリモーフィズム(言語によってはジェネリックとも呼ばれる)にも対応し、OCamlはさらにオブジェクト指向プログラミングも可能です。
bucklescriptのバインディングを使えば、既存のJavaScriptライブラリーも使えます。Reasonのコードと一緒にJavaScriptのコードも使えるので、挿入されたJavaScriptのコードは厳密に検査されませんが、ちょっと手直ししたいときや試作で十分役に立ちます。
Reactの開発者に、バインディングが使えてかつJSXにも対応していることも付け加えます。
/* A type variant being pattern matched */
let possiblyNullValue1 = None;
let possiblyNullValue2 = Some "Hello@";
switch possiblyNullValue2 {
| None => print_endline "Nothing to see here."
| Some message => print_endline message
};
/* Parametrized types */
type universityStudent = {gpa: float};
type response 'studentType = {status: int, student: 'studentType};
let result: response universityStudent = fetchDataFromServer ();
/* A simple typed object */
type payload = Js.t {.
name: string,
age: int
};
let obj1: payload = {"name": "John", "age": 30};
Haxe
Haxeはマルチパラダイム言語で、コンパイラーはバイナリコードのほか、さまざまな言語のソースコードが生成できます。
Haxeは型推論ありの厳密な型付けを持ちながらも、ターゲット言語が対応していれば動的型付け言語としても使えます。同じようにプログラミング方式もオブジェクト指向、ジェネリック、関数型などさまざまな方式に対応します。
Haxeでコードを書けば、大きな変更もなく複数のプラットフォームや言語を対象にコンパイルでき、特定ターゲット向けのコードブロックも書けます。
バックエンドとフロントエンドを同じHaxeで書き、同期通信・非同期通信とも対応したHaxe Remotingで相互に通信することも可能です。
Haxeのコードは既存ライブラリーと互換性がありますが、洗練された独自の標準ライブラリーも持っています。
// Example extracted from http://code.haxe.org
extern class Database {
function new();
function getProperty<T>(property:Property<T>):T;
function setProperty<T>(property:Property<T>, value:T):Void;
}
abstract Property<T>(String) {
public inline function new(name) {
this = name;
}
}
class Main {
static inline var PLAYER_NAME = new Property<String>("playerName");
static inline var PLAYER_LEVEL = new Property<Int>("playerLevel");
static function main() {
var db = new Database();
var playerName = db.getProperty(PLAYER_NAME);
trace(playerName.toUpperCase());
db.setProperty(PLAYER_LEVEL, 1);
}
}
Nim
Nimは静的型付けのマルチパラダイム言語です。簡潔で、空白も判別できる構文を持ち、C、C++、JavaScriptへコンパイルできます。
この言語自体は非常に簡潔ですがメタプログラミングが可能なので、ほかの言語にあるような機能も自分で実装できる点は魅力です。そのためにはマクロ、テンプレート、ジェネリックを使用して、簡単な機能から大掛かりな仕様まで構築できます。こうしたLisp的な思想によりNimは極めて万能な言語となっており、自分の用途に合わせて使えます。
Nimの持つ抽象化機能により、タスクに合わせた言語の改変が可能なため、真のDSL(ドメイン固有言語)を作成できます。解決すべき特別なタスクがある場合、より高度な記述表現が使えます。
# Reverse a string
proc reverse(s: string): string =
result = ""
for i in countdown(high(s), 0):
result.add s[i]
var str1 = "Reverse This!"
echo "Reversed: ", reverse(str1)
# Using templates
template genType(name, fieldname: expr, fieldtype: typedesc) =
type
name = object
fieldname: fieldtype
genType(Test, foo, int)
var x = Test(foo: 4566)
echo(x.foo) # 4566
最後に
JavaScriptが気に入らなくても、欠点に苦しむことなくWebアプリは作れます。紹介した通り、アプリ作りの選択肢は個人の好みに広く対応していて、PureScriptのような純粋関数型言語からDartのようなオブジェクト指向言語まで揃っています。さらに、言語の1対1の変換以上のものを求めるならば、ElmのようなバーチャルDOMと独自構造を持った言語が選べます。
(原文:10 Languages That Compile to JavaScript)
[翻訳:西尾 健史/編集:Livit]