【TypeScript】TS初心者の、TS初心者による、TS初心者のためのTS入門

f:id:ecb_tkaihatsu:20191219095330p:plain

はじめに

ブンブン Hello world. どうもこんにちは。開発です。

前回「もし新人プログラマが「プリンシプル・オブ・プログラミング」を読んだら」の記事を書かせて頂いたあの開発です。

プログラマ界隈では「1年に1言語」とは言いますが、かくいう私も一念発起して新しい言語を学ぼうと思った次第でございます。

しかし、ただ学ぶだけではペースも落ちますし、やる気の上下も大きいと考えました。

そこで、”学んだ内容を記事にする”というタスクを自分に課すことで学習効率を無理やり持ち上げようと画策致しました 。 そんなわけで今回は、「新人プログラマがどのように未収得の言語を学習するか」について津々浦々と文字列を列挙したいと思います。

What's TypeScript

TypeScriptとは、マイクロソフトによって開発/メンテナンスされているオープンソースのプログラミング言語である。

概要

TypeScriptは、一言で言ってしまえば「静的型付けJavaScript」だと私はとらえています。

JavaScriptをより使いやすく、効率的な開発ができることを目的とする「altJS(alternative JavaScript)」というものがあり、TypeScriptもそのうちの1つです。

altJSのプログラムはコンパイルすることでJSコードを生成することができ、別の言語のプログラミングを通じてJavaScriptを記述することができます。

TypeScriptでは、従来のJavaScriptに対して”string”や"number"などの型(Type)を明示的に定義し、型を守るようにプログラミングすることができます。

それでは型を導入することで何が良いのかについてまずは語っていきたいと思います。

JavaScriptとの違い

JavaScriptは動的な型付け言語として知られており、TypeScriptは静的な型付け言語です。

JavaScriptではプログラミング段階では変数や関数などの型が明示的に指定されておらず、実装が容易です。

しかし、JavaScriptは実行前の事前コンパイルがないため、実行してから意図しない挙動を示すことがままあると思っています。

これはC言語などの強い静的型付き言語ではコンパイルの段階でエラーがでることから比較的防ぐことが容易な内容が多いです。

一方のTypeScriptでは、前述のとおり明示的な型の指定が可能であり、コードを記述した段階で型の不一致などのエラーを検知することができます。

これにより実行前にある程度のバグを解消でき、本筋の開発に集中できます。

詳細

ここではTypeScriptについて少々細かく説明したいと思います。

文法

基本の文法はJavaScriptと同様です。

TypeScript独自の文法としては、型の宣言があげられます。

ここではTypeScriptの型システム、型宣言や型推論について見ていきましょう。

型宣言

まずは型の宣言から見てみましょう。

変数/関数 : Typeの形式で型を定義することができます。

関数については引数も型指定ができるため、コーディングの段階で誤った変数を渡してしまうことを防げます。

// 変数に型を定義
let hoge: string = "";
let fuga: number = 0;
let flag: boolean = false;

// 関数に型を定義
private function HogeFunction() : void {};

// 関数+引数に型を定義
private function CheckFunction(input: string): boolean { 
    if(input === ""){
        return false;
    }else{
        return true;
    }
};

また、TypeScriptではクラス、public,private,protectedなどのアクセス修飾子やインターフェースもサポートされています。

これによりC++やJavaのようなクラスベースのプログラミングをすることが可能となっています。便利ですね。

// クラスの定義
class Hoge{
    array: Array<number>;

    // プロパティの定義を省略
    constructor(public hogeValue: number, protected isFoo: boolean, arraySize: number){
        this.array = new Array(arraySize);
    }
}

// クラスの継承
class ExHoge extends Hoge{
    constructor(
        protected num: number, protected isFoo: boolean, 
        protected arraySize: number, protected state: string){
        super(num, isFoo, arraySize);
    }

    GetState(): string {
        return this.state;
    }
}

// インターフェースの宣言
interface IHoge {
    variableArray: Array<number>;
    hogeValue: number;
    GetValue(): number;
}

// インターフェースの実装
class Hoge_2 implements IHoge {
    variableArray: Array<number> = [];  // 変数配列
    hogeValue: number = 0;  // 評価値
    public state: string = "";
    protected isFoo: boolean = false;

    constructor(arraySize: number, init_value: number) {
        this.variableArray = new Array(arraySize);
        this.hogeValue = init_value;
    }
    
    GetValue():number {
        return this.hogeValue;
    }
}

型推論

TypeScriptの特徴の一つに、型推論があげられます。

これは右辺値を基準にその変数がどの型なのかを機械的に判別してくれるため、律儀に型を書く必要がありません。

必要最低限の記述で静的型付きの恩恵を受けることができます。

function GetNumber(): number { return 0; };  // number型定義
function GetString(): string { return "hoge; };  // number型定義

let typeNum = TypeNumber();  // number型と推定
let hoge = typeNum;  // number型と推定

let typeString = "";  // string型と推定
// typeString = TypeNumber();  // 型不一致でエラー

以下に実際にどのような様子でエラーが出るのかを示します。

f:id:ecb_tkaihatsu:20191217105852g:plain
TS型推論

この型推論のおかげで、必要最低限の変数の型定義、関数の型をしっかりと定義しておけば残りは言語側で良しなにやってくれます。

実際にやってみた

ここまでTypeScriptについて説明してきました。

言語の基本を覚えたら、実際に何か書いてみましょう。

こういうものは書いて覚えるといった側面もあります。

何はともあれ書いてみましょう

とりあえず何か書いてみるのも大事かなと思います。

頭より先に手を動かす的な。

今回は私が学生の頃にC++で実装したことのある「差分進化(Differential Evolution : DE)」をTypeScriptで実装したいと思います。

差分進化(DE)の実装、数値実験

差分進化は「連続関数最適化」と言われる領域で活躍する最適化アルゴリズムの一種です。

最適化アルゴリズムとは、対象問題の評価値を最小(もしくは最大)にするように変数繰り返し計算し最適化する計算手法です。

最適化アルゴリズムの実社会での応用例としては、新幹線N750系のフロント形状の設計などが有名でしょうか。

最適化アルゴリズムについて語りだしたらきりがないので、今回は割愛させていただきます。

機会があれば記事になる可能性が微粒子レベルで存在するかもしれません。

アルゴリズムとしては非常にシンプルですが、クラスの概念であったり、整数値、小数値であったりと何気に純正JSでの実装は手間がかかったりします。 これをTypeScriptでどれだけサクッとできるかを検証したいと思います。

実際に実装したソースコードが以下になります。(GitHub)

interface IPopulation {
  variable: Array<number>;
  evaluationValue: number;
}

class Population implements IPopulation {
  variable: Array<number> = [];  // 設計変数
  evaluationValue: number = 99999999;  // 評価値
  cr: number = 0;  // 交差率
  scallingFactor: number = 0;  // スケーリングファクターF
  popNumber: number = 0;  // 個体番号
  generation: number = 0;
  lowerBound: number = 0;  // 解空間の下限値
  upperBound: number = 0;  // 解空間の上限値

  constructor(var_size: number, init_value: number, init_CR: number, init_SF: number, init_popNum: number, init_lower: number, init_upper: number) {
    this.variable = new Array(var_size);
    this.evaluationValue = init_value;
    this.cr = init_CR;
    this.scallingFactor = init_SF;
    this.popNumber = init_popNum;
    this.lowerBound = init_lower;
    this.upperBound = init_upper;
  }
}

class TestFunctions {

  /** 
   * Rastrign
  variable: Array<number> : number  */
  public Rastrign(variable: Array<number>): number {
    let sum: number = 0;
    variable.forEach(variable => {
      sum += Math.pow(variable, 2) - 10 * Math.cos(2 * Math.PI * variable);
    });
    return 10 * variable.length + sum;
  }

  /**
   * DoublePower
  variable: Array<number> : number  */
  public DoublePower(variable: Array<number>): number {
    let sum: number = 0;
    variable.forEach(variable => {
      sum += Math.pow(Math.pow(variable, 2), 2);
    })
    return sum;
  }
}

function main(): void {
  const numOfPopulation: HTMLInputElement = <HTMLInputElement>document.getElementById("population");
  const numOfVariable: HTMLInputElement = <HTMLInputElement>document.getElementById('variable');
  const numOfGeneration: HTMLInputElement = <HTMLInputElement>document.getElementById('generation');
  let populationSize = Number(numOfPopulation.value);
  let variableSize = Number(numOfVariable.value);
  let maxGeneration = Number(numOfGeneration.value);

  DifferentialEvolution(populationSize, variableSize, maxGeneration);
}

async function DifferentialEvolution(init_popSize: number, init_varSIze: number, init_generation: number) {
  let popSize = init_popSize;
  let varSize = init_varSIze;
  let maxGeneration = init_generation;
  let firstBest: number = 0.0;
  let cr = 0.8;
  let scallingFactor = 0.5;
  let lowerBound = -5.2;
  let upperBound = 5.2;
  let TF = new TestFunctions();

  let population: Array<Population> = [];  // 母集団
  let childPopulation: Array<Population> = [];  // 子個体母集団
  let bestIndivArray: Array<Population> = [];  // ベスト解履歴

  await ResetTableRow();

  // 母集団の初期化
  population = Initialization(popSize, varSize, cr, scallingFactor, lowerBound, upperBound);
  childPopulation = Initialization(popSize, varSize, cr, scallingFactor, lowerBound, upperBound);
  // 評価
  population = PopulationEvaluation(population, TF);

  // ベスト個体の保存
  bestIndivArray.push(UpdateBest(population));
  firstBest = bestIndivArray[0].evaluationValue;

  childPopulation = DeepCopyObject(population);

  await Optimization(population, childPopulation, bestIndivArray, TF, maxGeneration);
  let output = <HTMLOutputElement>document.getElementById('result');
  output.innerHTML = String(bestIndivArray[bestIndivArray.length - 1].evaluationValue);
  console.log('first best value : ' + String(firstBest));
  console.log('Final best value : ' + String(bestIndivArray[bestIndivArray.length - 1].evaluationValue));
}

async function Optimization(population: Array<Population>, childPopulation: Array<Population>, bestIndivArray: Array<Population>, TF: TestFunctions, maxGeneration: number) {
  // 世代数分ループ
  for (let generation = 1; generation < maxGeneration; generation++) {
    childPopulation = CreateChildren(population, childPopulation);  // 子個体集団の生成
    childPopulation = PopulationEvaluation(childPopulation, TF);  // 評価
    population = UpdatePopulation(population, childPopulation);  // 母集団の更新
    bestIndivArray.push(UpdateBest(population));  // ベスト解の更新
    console.log('generation : ' + generation);
    await AddTableRow(generation, bestIndivArray[bestIndivArray.length - 1].evaluationValue);
  }
}

async function ResetTableRow() {
  let table: HTMLTableElement = <HTMLTableElement>document.getElementById('table1');  //表のオブジェクトを取得
  let row_num = table.rows.length;    //表の行数を取得
  while (table.rows[1]) {
    table.deleteRow(1);
  }
}
async function AddTableRow(generation: number, bestEval: number) {
  let table: HTMLTableElement = <HTMLTableElement>document.getElementById('table1');  //表のオブジェクトを取得

  let row = table.insertRow(-1);  //行末に行(tr要素)を追加

  let cell1 = row.insertCell(0);  //セル(td要素)の追加
  let cell2 = row.insertCell(1);

  cell1.innerHTML = String(generation);   //セルにデータを挿入する
  cell2.innerHTML = String(bestEval);
}

function Initialization(popSize: number, varSize: number, cr: number, scallingFactor: number, lowerBound: number, upperBound: number): Array<Population> {
  let population: Array<Population> = [];
  for (let pop = 0; pop < popSize; pop++) {

    population.push(new Population(varSize, 99999999, cr, scallingFactor, pop, lowerBound, upperBound));

    // 各個体の設計変数を初期化
    for (let varNum = 0; varNum < population[pop].variable.length; varNum++) {
      population[pop].variable[varNum] = GetRandomArbitrary(lowerBound, upperBound);
    }
  }
  return population;
}

function CreateChildren(population: Array<Population>, childPopulation: Array<Population>): Array<Population> {
  let popSize = population.length;
  let varSize = population[0].variable.length;
  let tmpChild = DeepCopyObject(childPopulation);

  for (let pop = 0; pop < childPopulation.length; pop++) {
    tmpChild[pop].variable = DeepCopy(childPopulation[pop].variable);
  }

  // 母集団のループ
  for (let popNum = 0; popNum < popSize; popNum++) {
    population[popNum].generation++;  // 世代数の更新

    let cross_varNum = GetIntegerRandomNumber(0, varSize + 1);  // 交差する変数の選択

    // 交叉のための個体番号を取得
    let rNum = SelectPopulationNumber(popSize);

    // 交叉による変数の変更
    tmpChild[popNum].variable = DeepCopy(population[popNum].variable);  // 親個体の変数を引継ぎ
    tmpChild[popNum].variable = CrossOver(population, tmpChild, popNum, cross_varNum, rNum);
  }
  return DeepCopyObject(tmpChild);
}

function SelectPopulationNumber(popSize: number): Array<number> {
  let rNum = [0, 0, 0];
  // 交叉のための個体番号を取得
  rNum[0] = GetIntegerRandomNumber(0, popSize);
  rNum[1] = GetIntegerRandomNumber(0, popSize);
  rNum[2] = GetIntegerRandomNumber(0, popSize);
  while (rNum[0] === rNum[1] || rNum[1] === rNum[2] || rNum[2] === rNum[0]) {
    rNum[0] = GetIntegerRandomNumber(0, popSize);
    rNum[1] = GetIntegerRandomNumber(0, popSize);
    rNum[2] = GetIntegerRandomNumber(0, popSize);
  }
  return rNum;
}

function CrossOver(population: Array<Population>, childPopulation: Array<Population>, popNum: number, cross_varNum: number, rNum: Array<number>): Array<number> {
  let variable = DeepCopy(childPopulation[popNum].variable);

  // 交叉による変数の変更
  for (let varNum = 0; varNum < population[popNum].variable.length; varNum++) {
    if (varNum === cross_varNum || population[popNum].cr > GetRandomArbitrary(0, 1)) {

      variable[varNum] = population[rNum[0]].variable[varNum] + population[popNum].scallingFactor * (population[rNum[1]].variable[varNum] - population[rNum[2]].variable[varNum]);

      // 上下限値の修正
      if (variable[varNum] < population[popNum].lowerBound) {
        variable[varNum] = population[popNum].lowerBound;
      }
      if (variable[varNum] > population[popNum].upperBound) {
        variable[varNum] = population[popNum].upperBound;
      }
    }
  }

  return DeepCopy(variable);
}

function PopulationEvaluation(population: Array<Population>, TF: TestFunctions): Array<Population> {
  let tmpPopulation = DeepCopyObject(population);

  tmpPopulation.forEach(population => {
    // 評価
    population.evaluationValue = TF.Rastrign(population.variable);
  });
  return DeepCopyObject(tmpPopulation);
}

// 母集団の解更新
function UpdatePopulation(population: Array<Population>, childPopulation: Array<Population>): Array<Population> {
  let tmpPopulation = DeepCopyObject(population);

  for (let popNum = 0; popNum < population.length; popNum++) {
    if (childPopulation[popNum].evaluationValue < population[popNum].evaluationValue) {
      tmpPopulation[popNum] = childPopulation[popNum];
    }
  }
  return DeepCopyObject(tmpPopulation);
}

// ベスト個体の保存
function UpdateBest(population: Array<Population>): Population {
  let bestPopNumber = GetBestPopulationNumber(population);
  console.log('best num : ' + bestPopNumber);
  console.log('best : ' + population[bestPopNumber].evaluationValue);
  return population[bestPopNumber];
}

// ベスト解の番号を取得
function GetBestPopulationNumber(population: Array<Population>): number {
  let bestPopNumber = 0;
  for (let popNum = 1; popNum < population.length; popNum++) {
    if (population[popNum].evaluationValue < population[bestPopNumber].evaluationValue) {
      bestPopNumber = popNum;
    }
  }
  return bestPopNumber;
}

function GetRandomArbitrary(min: number, max: number): number {
  return Math.random() * (max - min) + min;
}

function GetIntegerRandomNumber(min: number, max: number): number {
  return Math.floor(Math.random() * (max - min)) + min;
}

function DeepCopy(array: Array<number>): Array<number> {
  return JSON.parse(JSON.stringify(array));
}

function DeepCopyObject(arrayObject: Array<Population>): Array<Population> {
  return JSON.parse(JSON.stringify(arrayObject));
}

実験結果

上記ソースコードを実行してみた結果を見てみましょう。

リポジトリにある「test.html」から実際に上記コードを動かしてみます。

f:id:ecb_tkaihatsu:20191217111245p:plain
test.thml

「test.html」で表示された画面で、パラメータを入力します。

入力するパラメータは、

  • 個体群の数
  • 変数数
  • 世代数

の3種類です。

f:id:ecb_tkaihatsu:20191217093853p:plain
ParameterSetting

それぞれの入力が完了したら、「optimization」ボタンを押しましょう。最適化が始まります。

実行結果がParameterSettingの隣に表示されます。

f:id:ecb_tkaihatsu:20191217094234p:plain
Result1

                           ・

                           ・

                           ・

f:id:ecb_tkaihatsu:20191217094252p:plain
Result2

結果としては、各世代の最適解をテーブルにして出力しています。

これを見ると、最初の方の世代では最適化が進んでいるものの、終盤の方の世代では停滞していることが分かりますね。

まぁ今回の目的はある程度の最適化ができることなので良しとしましょう。

最適化の精度を上げるとなると、内部パラメータの調整という果てしない地獄の門を叩くことになるので…

(パラメータ自動調整の研究もありますがソースが複雑になるので今回は割愛)

感想

実際にTypeScriptでDEを実装してみましたが、型があることでプログラマが意識すべきことが減ったような気がしてコードは書きやすいなと感じました。

特に型推論が便利で、良い具合に保管してくれるため無駄に書くこともなくなります。

また、推論の結果型が合わない場合などはしっかりとエラーを出してくれるため、コードの修正が容易です。

この点が純粋なJSだと、実際に実行してみるまで自分のミスに気づくことが難しかったりするので恩恵を受けている部分ですね。

筆者はソースコードをVSCodeで書いていましたが、Intellisenseもなかなかに精度が良く書きやすかったのも評価ポイントです。

上記結果から、TypeScriptはJSに比べ書きやすい言語であり、そこそこの規模のソースを作るのに最適であると感じることができました。

普段静的型付き言語を触っているなら比較的慣れも早いと思うので、興味のある方は是非さわってみてください。

また、「こう使うと良い」「こう書くとより良い」などのご意見やアドバイスもあればどしどしコメントください!

余談

今回の例題で「TypeScriptのコードを書く際になぜDEを選んだのか」と言われると、ぶっちゃけ書いたことあるアルゴリズムだからというだけです。

ちょうどよい複雑さで、ある程度構造的に処理しないと面倒であり、別言語での書いた実績のあるものだからというのが選定理由ですね。

普段は静的型付き言語しか触らない筆者としては、JSの何でもあり感には若干の抵抗があるわけです。

今回のソースコードでも、箇所によって扱っている変数や関数の型を変えており、ある程度意識して管理しないと成り立たないものになっています。

この辺がTSだと、コーディング途中やコンパイル時にエラーとなって知らせてくれるのでただのミスを大幅に低減することができます。

おわりに

今回はエンジニアとして自己を高めることを目的として、TypeScriptについて学びました。

結果として、TypeScript初級編くらいの知識は付けれたと思います。

今回はほぼ純正のTypeScriptのみで実装を行いましたが、今後はライブラリも活用してもう少し規模の大きいプログラムも組みたいなと感じました。

1年に1言語を目的として、来年も何かしらの言語を習得したいと思います。

おまけ:言語の勉強をどうやるか

今回の記事作成に当たり、自分なりにどうやって未収得の言語を学べばいいかを考えました。

備忘録もかねてまとめておきたいと思います。

あくまで自己流なのでそのあたりはご了承ください。

言語仕様の把握

まずは何といっても仕様を把握することが先決です。

公式のドキュメントやはてなブログ、Qiitaなど分かりやすく解説している記事を探し、とりあえず読み漁りましょう。

評価の高い書籍を読むことも非常に効果的です。

この段階をクリアすると以下のような気分になるでしょう。

「完全に理解した」

文法の把握

仕様を把握したら、次は文法です。

ここからは実際に手を動かして言語に触れましょう。

経験を積むことが何より重要です。

人生において、大抵のことは慣れが解決してくれると個人的には考えているので、言語に慣れましょう。

  • 作る
  • 作る
  • 作る

プログラミングを進めていると、以下のような気分になるでしょう。

「なにもわからない…」

アウトプットする

ある程度まとまったプログラムを書くことができるようになったら、アウトプットしましょう。

学習はインプットとアウトプットのバランスが重要です。

これまでインプットを続けてきたので、ここからはアウトプットの時間です。

これまで学習してきた内容、作った成果物を世に公開するのです。

媒体は何でも構いません。

Git、ブログ、会社や大学の発表会etc...

とにかく誰かに見てもらってレビューを受けることが重要です。

また、外部に公開することを意識することで自分としてもある程度きれいに、体系的にまとめようという意識を持つこともできます。

その結果、言語についてある程度話すことができるようになるでしょう。 ← いまここ

このまま学習(インプット)と公開(アウトプット)を続けていくと、最終的に以下のような気持ちになるでしょう。

「チョット デキル…」


~ecbeingでは、インプットとアウトプットを繰り返して「チョットデキル」気持ちになりたいエンジニアを大募集中です!~

careers.ecbeing.tech