プログラムのバージョン管理とオープンソース
プログラムを複数人で開発する場合のバージョン管理と、オープンソースプログラムを使う場合の注意を説明する。
バージョン管理システム
プログラムを学校や自宅のパソコンで開発する場合、そのソースコードはどのように持ち運び管理修正すべきだろうか? また、そのソースコードを複数人で開発する場合は、どのように管理修正すべきだろうか?
最も原始的な方法は、常に全部を持ち歩く方法かもしれない。しかし、プログラムが巨大になってくるとコピーに時間がかかる。またコピーを取る時に、どれが最新なのか正しく把握する必要がある。
- 同期方式 – 2つのディレクトリのファイルの古い日付のファイルを、新しい日付のファイルで上書きするようなディレクトリ同期ソフトを使って管理
- 圧縮保管 – ファイル全体だと容量も多いため、複数のファイルを1つのファイルにまとめて圧縮を行う tar コマンドを使うことも多い。(tar ball管理)
diffとpatch
プログラムの修正を記録し、必要最小限で修正箇所の情報を共有する方式に patch がある。これには、2つのファイルの差異を表示する diff コマンドの出力結果(通称patch)を用る。diff コマンドでは、変更のある場所の前後数行の差異を !(入替) +(追加) -(削除) の目印をつけて出力する。patch コマンドに diff の出力を与えると、!,+,- の情報を元に修正を加えることができる。(通称「patchをあてる」)
((( helloworld-old.c ))) #include <stdio.h> void main() { printf( "Hello World\n" ) ; } ((( helloworld.c ))) #include <stdio.h> int main( void ) { printf( "Hello World\n" ) ; return 0 ; } ((( diff の実行 ))) $ diff -c helloworld-old.c helloworld.c ((( 生成された patch 情報 ))) *** helloworld-old.c 2022-07-25 10:09:10.694442400 +0900 --- helloworld.c 2022-07-25 10:09:26.136433100 +0900 *************** *** 1,5 **** #include <stdio.h> ! void main() { printf( "Hello World\n" ) ; } --- 1,6 ---- #include <stdio.h> ! int main( void ) { printf( "Hello World\n" ) ; + return 0 ; }
インターネットの初期の頃には、他の人のプログラムに対して間違いを見つけると、作者に対してこのpatch(diff出力)をメールなどで送付し、patchコマンドでプログラムの修正が行われた。
広く世界で使われている Web サーバ apache は、オープンソースで開発されてきた。当初はプログラム公開後に間違いや機能追加の情報(patch)が世界中のボランティア開発者から送られてきながら改良が加えられていった。このため、”a too many patches”「つぎはぎだらけ」という自虐的皮肉を込めて apache と名付けられたと言われている。
初期のバージョン管理システム
バージョン管理システムは、複数人で少しづつテキストファイルに修正を加えながら改良を行うような際に、誰がどのような修正を行ったかという修正履歴を管理するためのツール。unix などのプログラム管理では rcs (revision control system) が使われていたが、その改良版として cvs (concurrent version system) が使われるようになっていった。(現在は後に紹介する Git などが主流)
- ci コマンド(check in) – ファイルをバージョン管理の対象として登録する。
- co コマンド(check out) – ファイルを編集対象とする(必要に応じて書き込みロックなども可能)。co されたファイルは、編集した人が ci して戻すまで ci することができない。
- 修正結果を ci する際には、新しい編集のバージョン番号などをつけて保存される。
- co コマンドでは、バージョン番号を指定してファイルを取り出すことも可能。
[Bさんの修正] /check out \check in ファイルver1.0-----→ver1.1------→ver1.2 \check out /check in [Aさんの修正]
集中管理型バージョン管理システム
rcs,cvs では、ファイルのバージョンは各ファイルを対象としているため、ファイルやディレクトリの移動や削除は管理が困難であった。これらの問題を解決するために、集中管理を行うサーバを基点として、対象ファイルのディレクトリ全体(ソースツリー)に対してバージョン番号を振って管理を行う。subversion はサーバに ssh などのネットワークコマンドを介して、保存・改変を行うことができる。
しかし、複数の人の修正のマージ作業の処理効率が悪く、処理速度が遅いため使われなくなっていった。同様のバージョン管理システムが企業により有償開発されていた(BitKeeperなど)が製品のライセンス問題が発生し、業を煮やした Linux 開発の Linus が Git のベースを開発・公開している。
分散型バージョン管理システム
Gitは、プログラムのソースコードなどの変更履歴を記録・追跡するための分散型バージョン管理システムである。Linus によって開発され、ほかの多くのプロジェクトで採用されている。(以下wikipedia記事を抜粋加筆)
Gitは分散型のソースコード管理システムであるため、リモートサーバ等にある中心リポジトリの完全なコピーを手元(ローカル環境)に作成して、そのローカルリポジトリを使って作業を行う。
一般的な開発スタイルでは、大雑把に言えば、以下のようなステップの繰り返しで作業が行なわれる:
- git clone – リモートサーバ等にある中心リポジトリをローカルに複製する。
- git commit – ローカルでコンテンツの修正・追加・削除を行い、ローカルリポジトリに変更履歴を記録する。
- 必要に応じて過去の状態の閲覧や復元などを行う。場合によってはこのステップを何度か繰り返す。
- git push – ローカルの変更内容を中心リポジトリに反映させる。
- git merge – git push の段階で、作業者ごとの変更内容が衝突することもある。Gitが自動で解決できる場合もあれば、手動での解決する。
- git pull – 更新された中心リポジトリ(他者の作業内容も統合されている)をローカルの複製にも反映する。これによりローカル環境のコードも最新の内容になるので、改めてステップ2の作業を行う。
ローカルリポジトリ(Aさん) ver1.0a1 ver1.0a2 ver1.1a1 修正--(git commit)--修正--(git commit) 修正--(git commit) /git clone \git push /git pull Bさんの修正 中心リポジトリver1.0-----------------ver1.1 も含まれる \git clone /git push 修正--(git commit)--修正--(git commit) 編集の衝突が発生すると ver1.0b1 ver1.0b2 git merge が必要かも ローカルリポジトリ(Bさん)
GitHub
Git での中心リポジトリを保存・管理(ホスティング)するためのソフトウェア開発のプラットフォーム。コードの管理には Git を利用し GitHub 社によって保守されている。2018年よりマイクロソフトの傘下企業となっている。
GitHub では単なるホスティングだけでなく、プルリクエストやWiki機能(ドキュメントの編集・閲覧機能)といった、開発をスムーズに行うための機能も豊富である。(個人的な例:github.com/tohrusaitoh/)
GitHub で管理されているリポジトリには、公開リポジトリと非公開リポジトリがあり、非公開リポジトリはその管理者からの招待をうけないとリポジトリ改変に参加できない。
企業でのプログラム開発で GitHub を内々で使っている事例なども多いが、間違って公開リポジトリと設定されていて企業の開発中のプログラムが漏洩してしまった…との事例もあるので、企業での利用では注意が必要。
オープンソースとライセンス
オープンソースプログラムは、プログラムのソースコードをインターネットで公開されたものである。しかし、元となったプログラムの開発者がその利用に対していくつかの制約を決めていることが多い。これらのオープンソースプログラムでのソフトウェア開発手法の概念として「伽藍とバザール」を紹介する。
伽藍とバザール
伽藍(がらん)とは、優美で壮大な寺院のことであり、その設計・開発は、優れた設計・優れた技術者により作られた完璧な実装を意味している。バザールは有象無象の人の集まりの中で作られていくものを意味している。
たとえば、伽藍方式の代表格である Microsoft の製品は、優秀なプロダクトだが、中身の設計情報などを普通の人は見ることはできない。このため潜在的なバグが見つかりにくいと言われている。
これに対しバザール方式では明確な方針が決められないまま、インターネットで公開されているプログラムをボランティアを中心とした開発者を中心に開発していく手法である。
代表格の Linux は、インターネット上にソースコードが公開され、誰もがソースコードに触れプログラムを改良してもいい(オープンソース)。その中で、新しい便利な機能を追加しインターネットに公開されれば、良いコードは生き残り、悪いコードは自然淘汰されていく。このオープンソースを支えているツールとしては、前に述べた git が有名。
オープンソース・ライセンス
ソースコードを公開している開発者の多くは、ソフトウェア開発が公開することで発展することを期待する一方で、乱用をふせぐために何らかの制約をつけていることが多い。最初の頃は、開発者に敬意を示す意味で、プログラムのソースコードに開発者の名前を残すこと、プログラムを起動した時に開発者の名前が参照できること…といった条件の場合もあったが、最近ではソフトウェアが広く普及・発展することを願って条件をつけることも多い。
こういったオープンライセンスの元となったのは、Emacs(エディタ),gcc(コンパイラ)の開発者のストールマンであり、「ユーザーが自由にソフトウェアを実行し、(コピーや配布により)共有し、研究し、そして修正するための権利に基づいたソフトウェアを開発し提供することにより、ユーザーにそのような自由な権利を与えた上でコンピュータやコンピューティングデバイスの制御をユーザーに与えること」を目標に掲げた GNU プロジェクトがある。linux を触る際のコマンドで、g で始まるプログラムの多くは GNU プロジェクトのソフトウェア。
GNU プロジェクトが掲げる GNU ライセンス(GPL)では、GPLが適用されていれば、改良したソフトウェアはインターネットに公開する義務を引き継ぐ。オープンソースライセンスとして公開の義務の範囲の違いにより、BSD ライセンス、Apacheライセンスなどがある。
コピーレフト型 | GNU ライセンス(GPL) | 改変したソースコードは公開義務, 組み合わせて利用では対応箇所の開示が必要。 |
準コピーレフト型 | LGPL, Mozilla Public License | 改変したソースコードは公開義務。 |
非コピーレフト型 | BSDライセンス Apacheライセンス |
ソースコードを改変しても公開しなくてもいい。 |
GPLライセンス違反
GPLライセンスのソフトウェアを組み込んで製品を開発した場合に、ソースコード開示を行わないとGPL違反となる。大企業でこういったGPL違反が発生すると、大きな風評被害による損害をもたらす場合がある。
- SwitchBot 社製品のGPL違反の注意喚起 – といっても2年間放置されてたの?
- SwitchBot 社が、この2023年7月に、GPL違反の注意喚起を受け、ようやく対応したようだ
最近のライセンスが関連する話題を1つ紹介:GitHub を使った AI プログラミング機能「Copilot」というサービスが提供されている。Copilot のプラグインをインストールした vscode(エディタ) では、編集している関数名や変数名などの情報と GitHub で公開されているプログラムの 学習結果を使って、関数名を数文字タイプするだけで関数名・引数・処理内容などの候補を表示してくれる。しかし、Copilot を使うと非オープンライセンスで開発していたプログラムに、オープンソースのプログラムが紛れ込む可能性があり、非オープンソースプロジェクトが GPL で訴えられる可能性を心配し「Copilot は使うべきでない」という意見の開発者も出ている。Copilot だけでなく、生成系 AI によるプログラムでも、同様の問題が指摘されている。
理解度確認
D/A・A/D変換回路と誤差
小型コンピュータを使った制御では、外部回路に指定した電圧を出力(D/A変換)したり、外部の電圧を入力(A/D変換)したりすることが多い。以下にその為の回路と動作について説明する。
D/A変換回路
ラダー抵抗回路によるD/A変換の仕組みを引用
このような回路で、D0,D1,D2 は、デジタル値の0=0[V] , 1=5[V] であった場合、Output 部分の電圧は、(D0,D1,D2)の値が、(0,0,0),(0,0,1),…(1,1,1)と変化するにつれ、5/8[V]づつ増え、(1,1,1)で 5*(7/8)=4.4[V]に近づいていく。最後に、Output が出力によって電圧が変化しないように、アンプ回路を通す。
DCモータをアナログ量で制御しないこと
このように、電圧をコンピュータから制御するようになると、ロボットで模型用の直流モータの回転速度をこれで制御したい…と考えるかもしれない。
しかし、直流モータは、ブラシとコイル(電磁石)を組み合わせたものだが、モーターが回転しだす瞬間でみれば、コイルは単なる導線である。このため、小さい電流でゆっくりモータを回転させようとすると、たとえ小さい電圧でも導線(抵抗はほぼ0[Ω])には大量の電流が流れ、モータをスイッチングする回路は焼き切れるかもしれない。
PWM変調
こういう場合には、PWM変調(Pulse Width Modulation) を行う。電圧の高さは一定で、高速回転させるときは長時間電圧をONにするが、低速回転させるときはONとOFFを繰り返し信号でONの時間を短くする。
このような波形であれば、低速度でも電流が流れる時間が短く、大量の電流消費は避けられ、モーターをまわす力も安定する。
A/D変換回路
D/A変換とは逆に、アナログ量をデジタル値に変換するには、どのようにするか?
このような場合には、A/D変換回路を用いる。一般的な回路では、以下のような逐次比較型A/D変換を用いる。
この回路では、変換開始と共に入力値をサンプル保持回路でアナログ量を保存する。
その後、Registerの中のデジタル値を、D/A 変換回路でアナログ量に変換した結果を、比較器(Comparator)でどちらが大きいか判断し、その結果に応じて2分探索法とかハイアンドローの方式のように、比較を繰り返しながらデジタル値を入力値に近づけていく。
ハイアンドロー(数あてゲーム)
数あてゲームで、デタラメな0〜127までの整数を決めて、ヒントを元にその数字を当てる。回答者は、数字を伝えると、決めた数よりHighかLowのヒントをもらえる。
最も速い回答方法は…例えば決めた数が55だとすると
・初期状態 ??????? 0..127 ・64 - Low 0?????? 0..63 ・32 - High 01????? 32..63 ・48 - High 011???? 48..63 ・56 - Low 0110??? 48..55 ・52 - High 01101?? 52..55 ・54 - High 011011? 54..55 ・55 - Bingo 0110111 55確定どんな値でも、7回(27=127)までで当てることができる。
量子化と量子化誤差
アナログデータ(連続量)をデジタルデータなどの離散的な値で近似的に表すことを、量子化という。
量子化誤差とは、信号をアナログからデジタルに変換する際に生じる誤差のことをいう。
アナログ信号からデジタル信号への変換を行う際、誤差は避けられない。アナログ信号は連続的で無限の正確さを伴うが、デジタル信号の正確さは量子化の解像度やアナログ-デジタル変換回路のビット数に依存する。
偶然誤差
アナログ信号がA/D変換回路に入るまでに、アナログ部品の電気的変動(ノイズ)が原因で値が変動することもある。ノイズが時間的に不規則に発生し、値が増えてしまったり減ってしまったり偶然に発生するものは偶然誤差という。偶然誤差を加えると相殺されてほぼ0になるのであれば、統計的な手法で誤差の影響を減らすことができる。
系統誤差
ある特定の原因によって測定値が偏る誤差は、系統誤差と呼ぶ。例として、測定機器が持つ精度である器差によるもの、温度や湿度、気圧などが影響して発生するもの、測定方法の癖で値が偏るものなどがある。系統誤差は、誤差の要因が解ればその偏ったズレの量を調べて、測定した値からズレを引き算すれば補正することができる。
なぜデジタル信号を使うのか
コンピュータが信号処理でなぜ使われるのか?例えば、下の信号のように、電圧の低い/高いで0/1を表現したとする。
ノイズが混入しづらい
このデータ”01011100″を通信相手に送る場合、通信の途中でノイズ(図中の赤)のような信号が加わった場合、アナログ信号では、どれがノイズなのか判別することはできない。しかしデジタル信号であれば、真ん中青線より上/下か?で判別すれば、小さいノイズの影響は無視して、元どおりの”01011100″を取り出せる。この0か1かを判別するための区切り(図中青線)は、しきい値と呼ばれる。
ノイズを見つける・治す
また、”01011100″のデータを送る通信の途中で、しきい値を越えるような大きなノイズが混ざって、受信したとする。この場合、単純に受け取るだけであれば、”01010100″で間違った値を受け取っても判別できない。しかし、データを送る際にパリティビット(偶数パリティであれば全データの1の数が偶数になるように)1ビットのデータを加える。このデータを受け取った際に、ノイズで1ビット反転した場合、1の数が奇数(3個)なので、ノイズでビット反転が発生したことがわかる。これをパリティチェックと言う。
このように、デジタル信号を使えば、しきい値を越えない程度のノイズならノイズの影響を無視できるし、たとえ大きなノイズでデータに間違いがあっても、パリティチェックのような方法を使えば間違って伝わったことを判別できる。
パリティチェックは、元のデータに1bitの信号を追加することで誤り検出ができるが、2bit同時に変化してしまうと誤りを見つけられない。そこで、元データにさらに多くのbit情報を追加すると、1bitの間違いを元に戻すようにもできる。誤り検出・訂正
電子回路で制御するかコンピュータで制御するか
これ以外にも、デジタル信号にする理由がある。
アナログ回路(電子回路)で制御しようとすると、抵抗やコイルやコンデンサといった受動素子が必要となるが、その中でもコイルは小型化がしづらい部品で、制御回路全体の小型化が難しい。大量生産ができるような回路なら小型化ができるかもしれないが、多品種少量の生産物では小型化のための開発費用の元がとれない。しかし、大量生産された安価な小型コンピュータで制御すれば、制御回路全体の小型化も可能となる。
また、電子回路の特性を調整するには、抵抗などの部品をはんだ付けをしながら部品を交換することになるかもしれない。しかしながら、アナログ信号をデジタル信号にしてしまえば、ノイズを減らすための平均化処理などは計算で実現できるし、特性を変化させるための調整もプログラムの数値を変更するだけで可能となる。
Hook(フック)とは
卒研で汎用的な物を作っていく中で、本来の処理とは別に、この対象の時は、特別な処理をしたいということが出てくる。しかし、この処理を if ( … ) で管理するとプログラムのコードが煩雑となることも多い。この時のターゲットに応じた特別な処理は、Hook(フック)と呼ぶ。
「Hook(フック)」は、プログラミングにおいて、既存のプログラムの特定の処理の途中に、別のカスタムな処理を差し込む(「引っ掛ける」)ための仕組みを指します。まるでプログラムの実行の流れに「釣り針」を引っ掛けて、自分のコードを実行するようなイメージです。(Geminiより)
struct Target{ int id ; // 識別用のID : } ; void action_Target( Target* t ) { if ( t.id == T_FOO ) { // 特別前処理_FOO } else if ( t.id == T_BAR ) { // 特別前処理_BAR } // 通常処理 : if ( t.id == T_FOO ) { // 特別後処理_FOO } else if ( t.id == T_BAR ) { // 特別後処理_BAR } }
このようなプログラムでは、action_Target() が、どんどん肥大化して、メンテナンスが面倒になる。
C言語であれば、関数ポインタを用いて、以下のような実装とするかもしれない。
struct Target { int id ; void (*action_pre_hook)( Target* t ) ; void (*action_post_hook)( Target* t ) ; : } ; void action_foo_pre_hook( Target* t ) { // 特別前処理_FOO } void action_foo_post_hook( Target* t ) { // 特別後処理_FOO } struct Target target_FOO = { T_FOO , action_foo_pre_hook , // 特別前処理が必要なら関数を登録しておく action_foo_post_hook , : } ; void action_Target( Target* t ) { if ( t->action_pre_hook != NULL ) // 特別前処理を呼び出す t->action_pre_hook( t ) ; // 通常処理 if ( t->action_post_hook != NULL ) // 特別前処理を呼び出す t->action_post_hook( t ) ; }
オブジェクト指向を使える言語であれば、仮想関数などを用いて実装するのが普通だろう。
class Target { virtual void action_pre_hook() = 0 ; // 仮想関数 virtual void action_post_hook() = 0 ; void action() { action_pre_hook() ; // 通常処理 action_post_hook() ; } } ; class Target_FOO : public Target { virtual void action_pre_hook() { // 特別前処理 } virtual void action_post_hook() { // 特別後処理 } } ;
JavaScript などの言語であれば、無名関数やラムダ式などでフックを登録するのが普通であろう。
const target_foo = { pre_hook: function( target t ) { // 特別前処理 } , post_hook: function( target t ) { // 特別後処理 } , : } ; function action( target t ) { t.pre_hook( t ) ; // 通常処理 t.post_hook( t ) ; }