何某日和

「カメラ」のち「ハンダゴテ」ところにより「プログラミング」 ── そんな私“かめきち”のウェブサイト

2013.01.16:データセット

概要

 今回の題材は「データセット」です。実は「これまでと同じようにGUI部品を貼り付けたらオシマイ」という話ではありません。 テキストベースのプログラミングによってGUIを構成すること、オートマトン(状態遷移機械)をプログラムに援用して問題解決を図ること、など、 今回はちょっとテッキー(techie;技術屋)っぽいことに走ってみましょう。 ちなみにテキストはこちら(http://www.cc.kyoto-su.ac.jp/~atsushi/Programs/DataSet/index-j.html) ですが、今回は少しアレンジして利用しています。また、参考文献も(http://www.cc.kyoto-su.ac.jp/~atsushi/Smalltalkers/pdfs/20120801_SSK_fromSeno.pdf

準備

マシンメンテナンス

 本当にくどいですが...(日々のマシンメンテナンス

今日の目標

 今日は、工程表アプリケーションを拵えます。工程名、開始日、終了日、担当者といった各項目を編集したり、CSV形式で読み書きしたり。

完成したアプリケーション(データの表示) 完成したアプリケーション(行の挿入と編集)

さっそく始めましょう。

準備プログラムの実行

 いつも通りに以下のプログラムを実行して、準備を整えてください。いつも通りに。

| aCollection |
(aCollection := OrderedCollection new)
    add: #url: -> 'http://www.cc.kyoto-su.ac.jp/~g0947424/tips/archives/PairProgramming2012/20130116_DataSet/DataSet.st';
    add: #comment: -> 'Copyright 2008-2012 KSU (Kyoto Sangyo University). All Right Reserved.';
    add: #bundle: -> #KSU;
    add: #package: -> 'KSU-Template';
    add: #nameSpace: -> #KSU;
    add: #category: -> 'KSU-Template';
    add: #class: -> #{KSU.WorkDataSet};
    add: #protocol: -> #examples;
    add: #selector: -> #example1;
    add: #execute: -> [#{KSU.WorkDataSet} value example1];
    yourself.
InputState default altDown ifTrue:
    [| aString |
    aString := (aCollection first value copyFrom: 1 to: aCollection first value size - 3).
    aCollection at: 1 put: (#url: -> (aString , 'SemiComplete.st'))].
JunSystem
    perform: ((aCollection collect: [:each | each key]) inject: String new
            into: [:selector :key | selector , key]) asSymbol
    withArguments: (aCollection collect: [:each | each value]) asArray
準備完了

今回のGUI部品:DataSet

  • GUI部品の設置(データセット)
  • CUIからGUI構築
  • { alt, shift, control, command }キーの使い所
  • GUIからGUI構築
  • ちゃんと動きますか?

GUI部品の設置(データセット)

 貼り付ける部品は1つだけですので、サクッと済ませてしまいましょう。また、部品の設定(ID、モデル、位置)も調整を。

Palette Window MainWindow selectionInList Basics selectionInList Basics

 部品の配置が終わったのでexample1を...と思って実行してみると、枠内には何も表示されないはずです。 実を言うとデータセットは、あらかじめ列の定義をしておかないと正しく表示してくれないのです。

列の追加(New Column) 4列が追加された状態

 データセットの設定外面の中で「New Column」というボタンがありますが、これを押下すると列が挿入される仕組みになっています。 ちなみに、今回拵える工程表アプリケーションは「工程名」「開始日」「終了日」「担当者」の4項目で構成されているので、4列追加しておいて下さい。

工程名開始日終了日担当者
分析(オブジェクト指向分析)2012/07/202012/07/31山田太郎
設計(オブジェクト指向デザイン)2012/08/012012/08/15山田次郎
実装(オブジェクト指向プログラミング)2012/08/162012/08/31山田三郎

 今追加した列に、具体的な項目名「工程名」「開始日」...を割り当てていきたいのですが、その方法がわかりますか? GUI Painter Toolには、それらしいタブが見られますが、無効になったまま...はて、どうやら使えそうにありませんね...(ぇ

CUIからGUI構築

 GUIが使えないのなら、CUIを使って作り込めば良いのです。windowSpecのSourceタブを開いて、ソースコードから項目名を割り当ててゆきましょう。 ついでに、その項目に表示させるモデル(データを管理するオブジェクト)の割り当ても済ませておきます。

#(#{UI.DataSetColumnSpec}
    #properties: #(#{UI.PropertyListDictionary} #allowSorting true )
    #model: #'selectedRow workName'
    #label: '工程名'
    #labelIsImage: false
    #width: 300
    #editorType: #InputField
    #noScroll: false )
「工程名」の項目を追加したところ

 #model:に設定した「selectedRow」は、WorkProcessクラスのオブジェクトで、ある工程(データセットに含まれる任意の1行)を指しています。 そのすぐ右に付している「workName」は、WorkProcessのオブジェクトに対するメッセージで、工程名を文字列として応答してもらうためのものです。 つまり、列「工程名」には、ある工程(selectedRow)の工程名(workName)を表示させるということですね。

工程名workName
開始日workStart
終了日workEnd
担当者workPerson

 各項目に対応するメッセージは上表の通りですので、先の例に従って「開始日」の設定も行いましょう。ちなみに幅は「100」pxです。

「開始日」の項目を追加したところ

{ alt, shift, control, command }キーの使い所

 GUI Painter Toolを用いて列を追加することはできても、その具体的な項目名を定めることはできない...という流れで説明を続けてきましたが、ここで訂正します。 実はGUI上からも設定することが可能なのです。次の手順で操作することになりましょう。

  1. windowSpecをEditして、selectionInList(DataSet)を選択する。
  2. altキー(optionキー)を押下しながら、編集したい列をクリックして選択する。
  3. GUI Painter Toolのウィンドウの「Column」のタブを選択して、各項目を編集する。

 まさか「altキー」を使うことになろうとは思いませんでした。どうしてaltキーを押さねば列を選択できないのでしょうか。 このツールを使う人間(Smalltalkの利用者)は、これまでの文脈からaltキーの必要性を察することができたでしょうか。

 開発者にとって、altキーの押下状態を調べることなど容易なことですから、実際のアプリケーションにも援用したくなるものです。 具体例を挙げれば、今回のGUI Painter Toolも然り、以前に取り上げた「地球をクリック」も然り。 しかし、ユーザにとって正しい設計と言えるでしょうか。ユーザへの配慮を欠いているように思われませんか? ソフトウェアの設計に人間を登場させなかったゆえの不始末だろうと思います。非常に残念な例ですね。

 ただ、このaltキーの「わかりづらさ」を逆手に取ることもできましょう。例えば、アプリケーションの隠しコマンド入力に援用したり...。 普通にDo itすれば受講者用のデータが読み込まれるけど、altキーを押しながらDo itすると指導者用のデータが読み込まれる...など。 今日の冒頭にワークスペース上で実行したプログラムを、もう一度よく読んでみると良いでしょう。 何かしらプログラムを拵える際に、上手く活用してもらえると嬉しいです。ただし、ユーザへの無理強いはしないように。

GUIからGUI構築

 「GUIが使える」と判明しましたので、残り2列はGUIで編集しましょう。先に挙げた手順に従って、Columnタブの各項目を正しく設定してください。

列を選択した状態 GUI Painter ToolのColumnタブの各設定値 列の情報が更新された

終了日(幅:100px)と、担当者(幅:80px)を正しく設定できたら次へ進みましょう。

ちゃんと動きますか? ─ 動きません...

完成したように見えるアプリケーション 挿入や削除ができない

 example1を動かしてみて下さい。 正しく動いているように見えるのですが、いずれかの行を選択した後にメニューから「行を挿入」「行を削除」を実行してみると、いずれも動作しないことがわかると思います。

コードリーディング

 何故動かないのか...その原因を探りつつコードリーディングを行いましょう。 動くアプリケーションと対比してコードリーディングするのではなく、目の前のプログラムからその動きを想像するのです。それでは、参りましょう。

(今回も自分自身で説明して頂きます。が、わからなければ遠慮せずに申し出て下さい。)

  • example2, クラスWorkProcess
  • workProcesses, workProcesses:, selectionInList, selectedRow
  • addRow:, addRow:after:, removeRow:
  • getChar:, getRowCSV:, putRowCSV:with:
  • readDataSetFrom:, writeDataSetTo:
  • deleteRow, insertRow, openDataSet:, saveAsDataSet:, saveDataSet:
  • editCell, dataSetView, dataSetController
  • printOn:

example2, クラスWorkProcess

 よくわかっていないプログラムにexampleがあるなら、クラスの特徴を集約していることが多いので、そこから読み始めると良いでしょう。 その中で更に理解したいメッセージを見つけて、その中を追ってゆく...という具合に読んでゆきます。

 以降のメソッドではクラスWorkProcessのオブジェクトが頻出となりますが、さて、何を表現しているクラスでしょうか?

workProcesses, workProcesses:, selectionInList, selectedRow

 SelectionInListの考え方を理解することが肝要です。よくわからなければ、実体をひっつかまえて質問攻めに。詳しく見てみると、ValueHolderを包含しているではありませんか...。

addRow:, addRow:after:, removeRow:

 指定したWorkProcessのオブジェクトを追加したり削除したりしています。メニューから「行を挿入」「行を削除」しても動かなかったけど、プログラムは作ってある...何故動かないのか...。 ちなみに「after」に注目して下さい。「after」ではなく「before」なら、どのように書けばよいでしょうか?(今日の補足にも記しておきます。)

getChar:, getRowCSV:, putRowCSV:with:

 PrimeMinistersでお馴染み「CSV形式のデータ」の読み書きを担っています。(あとで改良します。) getChar:が改行コードの統一を行なっているのですが、読めるでしょうか。

readDataSetFrom:, writeDataSetTo:

 指定ファイル名のデータを読み込んだり、指定ファイル名でデータを書き込んだり。なんだかとても便利そうですね...。

deleteRow, insertRow, openDataSet:, saveAsDataSet:, saveDataSet:

 「"***こしらえてください***"」と一言。そう、アプリケーションが正しく動かなかったのは、この部分が原因だったのです。 でも、思い出してみると、これまでに見てきたメソッドを使って簡単に実装できそうじゃありませんか??(次節のコードライティングで修正を施します。)

editCell, dataSetView, dataSetController

 今回のDataSetや巷の表計算ソフトは、沢山のテキストフィールドが並んだアプリケーションだ...と思っていませんか?

DataSet? 表計算ソフト?(ニセモノ)

それは見当違いです。実を言うと、テキストフィールドは1つしか存在せず、それを表全体で共有しているに過ぎないのです。

DataSet、表計算ソフト(ホンモノ)
非表示の共有物:
     
     
     
     

空いているマスを選択すれば、共有のテキストフィールドが選択箇所に移動しますね。その共有フィールドこそが「editCell」です。無駄のない良い感じの仕組みだと思いませんか? (ちなみに、実際にeditCellメッセージで応答されるのは、editCellの現在位置を表した座標(Point)です。)

printOn:

 言わずと知れた自己紹介メソッドですね。このprintOn:またはprintStringを拵えておけば、デバッグの際に重宝します。

コードライティング

 お待ちかね(?)のコードライティングです。

  • insertRow
  • deleteRow
  • openDataSet
  • saveAsDataSet
  • ちゃんと動きますか?

insertRow

 ヒントはadding。

insertRowの実装例

deleteRow

 ヒントはremoving。

deleteRowの実装例

openDataSet

 ヒントはfileIn。

openDataSetの実装例

saveAsDataSet

 ヒントはfileOut。

saveAsDataSetの実装例

ちゃんと動きますか?

 メニューの「行を挿入」「行を削除」「開く」「別名で保存」が、それぞれ正しく動くようになったでしょうか。

CSVの正しい読み方

 今回のコードライティングがあまりに易しく力が抜けてしまったやに思われますので、ここで引き締めて更に本格的なコードライティングを行いましょう。

  • 不完全なCSVサポート
  • 「カンマで区切りゃ良い」ってもんじゃない ── 一般的なCSV形式
  • CSVデータの正しい書き出し
  • CSVデータの正しい読み込み(1) ── 現状の把握
  • CSVデータの正しい読み込み(2) ── 様々なCSVデータの例
  • CSVデータの正しい読み込み(3) ── オートマトンを援用した実装
  • ちゃんと動きますか?(再)
  • 【補足】テキスト版「getRowCSV:」 ── ユーザのための実装

不完全なCSVサポート

 まず、example1を実行して以下のような工程表を拵えてください。

工程名開始日終了日担当者
分析,設計2012/07/202012/07/31太郎,次郎
実装2012/08/012012/08/15三郎
サンプルデータ

このデータを一旦ファイルに保存し、アプリケーションを終了してください。もう一度アプリケーションを足しあげて、今保存したファイルを開いてみると...

CSVデータを正しく読めていない?

これはマズイ...orz

「カンマで区切りゃ良い」ってもんじゃない ── 一般的なCSV形式

 保存したCSVファイルの中身を見てみると...

分析,設計,2012/07/20,2012/07/31,太郎,次郎
実装,2012/08/01,2012/08/15,三郎

...困りましたね、データの中にカンマ「,」が含まれるわけですから、CSVファイルに出力されて当然なのですが、そのカンマが区切り文字として認識されてしまったようです。

実装2012/08/012012/08/15三郎

この4列のデータをCSV形式に直すと、以下のように単純なカンマ区切りの文字列として得られます。

【CSV形式】 実装,2012/08/01,2012/08/15,三郎

では、次の例はどうでしょう。

分析,設計2012/07/202012/07/31太郎,次郎
【誤ったCSV形式】 分析,設計,2012/07/20,2012/07/31,太郎,次郎
【正しいCSV形式】 "分析,設計",2012/07/20,2012/07/31,"太郎,次郎"

 データの中にカンマを含む際、正しいCSV形式では引用符「"」で両端を囲みます。つまり、引用符に囲まれたカンマは単なるデータとして扱い、そうでないカンマは区切り文字として扱うのです。 加えて言えば、カンマだけでなく改行文字・タブ文字・空白文字を含む場合も、引用符で囲む場合があります。

 さらに加えて、データの中に引用符「"」が含まれている場合には、「""」に置換することでエスケープします。なので、下のような例なら...

秋の"京都",仏閣巡り2012/11/232012/11/25四郎
【正しいCSV形式】 "秋の""京都"",仏閣巡り",2012/11/23,2012/11/25,四郎

上のように変換されることになります。

CSVデータの正しい書き出し

 正しいCSV形式を理解したところで、実際のプログラムに反映させてゆきましょう。 修正するのは、putRowCSV:with:ですが、少しゴタゴタした処理なのでcsvString:というメッセージを新しく拵えて、そちらで処理を施しましょう。

  • 対象のマス目(セル)のデータに、カンマ・改行文字・タブ文字・空白文字・引用符が含まれているか検査する。
    • 含まれておれば、そのデータを引用符で囲んでaStreamに流す。ただし「"」は「""」に置換する。
    • 含まれていなければ、そのデータをそのままaStreamに流す。

【putRowCSV:with:の実装】

putRowCSV: aStream with: aRow

    aRow do: 
            [:each |
            | aString |
            aString := each isString ifTrue: [each] ifFalse: [each printString].
            aStream nextPutAll: aString]
        separatedBy: [aStream nextPut: $,].
    aStream cr

【新しいメッセージcsvString:の実装】

csvString: aString

 さて、これでCSV形式の正しい書き込みができるようになったはず。先ほどのデータをもう一度保存してみましょう。

カンマを含んだCSVデータ データを保存する
"分析,設計",2012/07/20,2012/07/31,"太郎,次郎"
実装,2012/08/01,2012/08/15,三郎

ちゃんと正しい形式で保存できましたね!!

CSVデータの正しい読み込み(1) ── 現状の把握

 もう一度アプリケーションを立ち上げて、正しく保存できたCSVデータを開いてみましょう。(ダメ元で。)

正しいCSVデータを正しく読めていない様子

やっぱりダメでした。CSVのデータを読み込んでいるメッセージgetRowCSV:を再度読んでみましょう。

カンマデータ(セル)の区切りなので、バッファの内容物をコレクションに追加して、バッファを空にする。
カンマ・改行 以外データ(セル)内の文字列が続いているので、バッファに追加する。
改行1レコード(1工程)のデータを完全に読みきったので、バッファの中身をコレクションに入れてから処理終了。
現状の状態遷移図

【現在のCSVデータ読み取り処理の遷移】

【パスタ,京都,肉,ほげ\n】

 aCharacter = 
    aBuffer = 
aCollection = { }
 

CSVデータの正しい読み込み(2) ── 様々なCSVデータの例

 実際に存在しうる様々なCSVデータの例をご覧に入れましょう。その上で、CSV形式の正しい読み込み方について考えてゆきます。

まずは普通のデータ

あいうえおかきくけこさしすせそ
あいうえお,かきくけこ,さしすせそ

カンマ混じりのデータ

あいうえお,か,き,く,け,こ,さしすせそ
あいうえお,",か,き,く,け,こ,",さしすせそ

引用符「"」混じりのデータ

あいうえお"か"き"く"け"こ"さしすせそ
あいうえお,"""か""き""く""け""こ""",さしすせそ

改行混じりのデータ。青字の「\n」は改行文字。

あいうえおかきく
けこ
さしすせそ
あいうえお,"かきく\nけこ",さしすせそ

空セルのあるデータ

あいうえお     さしすせそ
あいうえお,,さしすせそ

全てが空セルのデータ

               
,,

無茶苦茶なデータ(これでも正しいCSV形式)

あ"い,う
えお
かきくけこ     
"あ""い,う\nえお","かきくけこ",

さあ、これらを上手く区切る解析器を拵えましょう。

CSVデータの正しい読み込み(3) ── オートマトンを援用した実装

 いきなりですが、CSV形式を読み込む際のオートマトンを描いてみましょう。ちょうど皆さんが履修している「言語オートマトン」を活かす機会がやってきたのです。 以下の手順に従って、オートマトンの図(状態遷移図)を描いてみてください。

  1. 初期状態を用意する。セルデータを読み込む直前の状態。
  2. 既に用意した状態のうち1つに注目して、何らかの文字が入力された場合の遷移先状態を用意する。
  3. 新しい状態が作られなるまで2.を繰り返す。

 いきなりですが、CSV形式を読み込む際のオートマトンを描いてみましょう。ちょうど皆さんが履修している「言語オートマトン」を活かす機会がやってきたのです。

CSV読み込みのオートマトン1

 完成版オートマトンの画像

 オートマトンが完成したところでプログラミングを始めましょう。メッセージgetRowCSV:を修正します。

【getRowCSV:の実装】

getRowCSV: aStream

    | aCollection aBuffer aBoolean |
    aCollection := OrderedCollection new.
    aBuffer := String new writeStream.
    aBoolean := true.
    [aStream atEnd not and: [aBoolean]] whileTrue: 
            [| aCharacter |
            aCharacter := self getChar: aStream.
            aCharacter = Character cr
                ifTrue: [aBoolean := false]
                ifFalse: 
                    [aCharacter = $,
                        ifTrue: 
                            [aCollection add: aBuffer contents.
                            aBuffer close.
                            aBuffer := String new writeStream]
                        ifFalse: [aBuffer nextPut: aCharacter]]].
    aCollection add: aBuffer contents.
    aBuffer close.
    ^aCollection

これで実装は完了しました。Formatした後にAcceptしてください。

ちゃんと動きますか?(再)

CSVファイルを正しく読み込めた様子

 どうでしょう。ひたすら冗長に書き認めたオートマトンでしたが、上手く読み込めてスッキリしたのではないでしょうか。 「オートマトンなんて何に使うねん」という素朴な疑問に答える簡単な例になったと思います。これを機に、講義科目「言語オートマトン」に親しみを覚えてもらえると幸いです。

【補足】テキスト版「getRowCSV:」 ── ユーザのための実装

 冒頭で紹介したテキストでは、今回修正したものとは異なるgetRowCSV:が実装されています。そのコードを読んでみましょう。

getRowCSV: aStream

    | aCollection aBuffer aBoolean |
    aCollection := OrderedCollection new.
    aBuffer := String new writeStream.
    aBoolean := true.
    [aStream atEnd not and: [aBoolean]] whileTrue: 
            [| aCharacter |
            aCharacter := self getChar: aStream.
            aCharacter = Character cr
                ifTrue: [aBoolean := false]
                ifFalse: 
                    [aCharacter = $,
                        ifTrue: 
                            [aCollection add: aBuffer contents.
                            aBuffer close.
                            aBuffer := String new writeStream]
                        ifFalse: 
                            [aCharacter = $"
                                ifTrue: 
                                    [| aLoop |
                                    aLoop := true.
                                    [aStream atEnd not and: [aLoop]] whileTrue: 
                                            [aCharacter := self getChar: aStream.
                                            aCharacter = $"
                                                ifTrue: 
                                                    [aStream peek = $"
                                                        ifTrue: 
                                                            [aStream next.
                                                            aBuffer nextPut: $"]
                                                        ifFalse: [aLoop := false]]
                                                ifFalse: [aBuffer nextPut: aCharacter]]]
                                ifFalse: [aBuffer nextPut: aCharacter]]]].
    aCollection add: aBuffer contents.
    aBuffer close.
    ^aCollection

 今回修正したものに比べて簡潔に書かれています。が、本来受理できないようなデータを受理してしまうため、CSVのパーサとしては正しくないのです。 例えば...

あいうえお,かき"くけこ",さしすせそ

このような「正しくないCSVデータ」を読んでみると、あたかも正しいCSVデータを読んだかのように振る舞ってしまうのです。

 さて、ここで考えるべきは、「ユーザが利用するアプリケーションではどちらの実装が良いのか」ということです。 お役所の手続きのように少しのミスも許されない厳密なアプリケーションが良いですか? 少しのミスなら寛大に受け止めてくれる柔軟なアプリケーションが良いですか?

私達が普段使っているCやJavaのコンパイラは「厳密なアプリケーション」のように見えて、実を言うと「柔軟なアプリケーション」なのです。 プログラマの作るプログラムには沢山のミスが含まれているもの。それをコンパイラにかけようものなら、含まれている沢山のミスを一度に幾つも指摘してくれますね。 [1]

$ gcc test.c
t.c: In function 'main':
t.c:5: error: expected ';' before 'printf'
t.c:7: error: expected ';' before 'printf'

$ cat test.c
#include <stdio.h>
int main() {
    printf("1");
    printf("2")    ←セミコロンが抜けているので、これ以降の青文はプログラムとして正しく読めないはず。
    printf("3");
    printf("4")    ←なのに、この行のセミコロン抜けも指摘してくれている。なぜ...
    printf("5");
    return 0;
}

ユーザの利便性のために厳密性を犠牲にしなければならない場合があります。ソフトウェア開発は、ソフトウェアを使うユーザが存在して初めて成り立つもの。 設計に「ユーザの存在」を組み込んで、どのような仕様が望まれるのかを正しく検討する必要があるのです。間違っても開発者主体のソフトウェア開発を行なわないように。


[1] 参考文献「はじめてのコンパイラ ─ 原理と実践」 宮本衛市 2007 森北出版 ISBN:9784627817210

【補足】before系、after系

 (おそらく起こり得ませんが)もし時間が余った場合には、ここに示すもう一つのコードライティング題材に取り組んでください。

 ある行を選択して「行を挿入」の操作を行うと、選択行の次の行として新しい行が追加されます。

行を挿入する 行を挿入した

 何も選択肢ないまま「行を挿入」の操作を行うと、最後尾に新しい行が追加されます。

行を挿入する 行を挿入した

1つ問題点がありませんか? 先頭行に新しい行を追加するにはどうすればよいのやら...。

before系とafter系

選択した行の次の行として挿入するものを「after系」と呼び習わせば、逆に、選択行の前の行として挿入するものは「before系」と表現できます。 今回の工程表アプリケーションは、メッセージaddRow:after:からもわかるように「after系」のアプリケーションでした。 この「after系」のアプリケーションで「先頭行の挿入」を行うには...

選択行あり選択行なし
before系選択行の前の行として挿入最後尾に挿入
after系選択行の次の行として挿入先頭に挿入

何も選択していない場合の「行を挿入」で、先頭行の挿入を行えば良さそうですね。加えてbefore系の挿入方法も掲載しておきました。さて、あとは作るのみ。 現在のアプリケーションは、「選択行あり」の場合に「after系」で、「選択行なし」の場合に「before系」で動作しています。 このどちらかを統一し、完全な「before系」or「after系」のアプリケーションに仕立てあげてください。

さいごに

 かなり濃厚なペアプログラミングになってしまいましたが、頭の中にちゃんと残っているでしょうか。 データの保存はもちろんですが、一番大切なのは「プログラムの成長過程」や「開発作業手順」です。 簡単なコマ送り形式によって、書籍からは学びとれない「プログラムの成長過程」を垣間見たわけですから、 きっと自身で何かしらのプログラムを組むことになった時に、微塵でも糧になるはずです。

 さて、その「コマ送り」の機能は、今回のペアプログラミングのためにJavaScriptで拵えたものだったりします。 (こちらで紹介。) 自身の作成物を誇示するようでみっともないのですが、伝えたいのは「学んだことを活かしてほしい」だとか「宝を持ち腐らせないでほしい」といったことです。 今回のペアプログラミング「データセット」は、ウェブアプリケーションやオートマトンの知識を活かしてまとめあげました。 皆さんも来年のペアプログラミングでは自分の得てきた知見を存分に活用し、指南する側も教わる側も双方に意義のあるものとして取り組んでもらえればと思います。

さいごに:プログラムの保存

 データの保存をお忘れなきよう。