この記事の概要
paiza x Qiitaコラボキャンペーンの問題「N倍の文字列」を、Excelワークシート関数を使って4通りの手法で解き、プログラミング的な観点を加えて考察した。
これを書いたきっかけ
ここ数年でExcelワークシート関数が超強化され、プログラミング言語と呼べるようになったのだが、その割に盛り上がりが少ない。ということで、paiza x Qiitaコラボキャンペーンの問題を使って、Excelのワークシート関数で実装例を紹介してみたする下準備としてワークシート関数について考えてみたら思ったより長くなったので、独立した記事にしてみた。
なお、筆者はプログラミング素人であり、誤り等が含まれている可能性がある。ご意見・ご指摘等は大歓迎である。
ワークシート関数の特徴と使い方を、プログラミングと対比して考える(「N倍の文字列」を例に)
簡単そうなN倍の文字列を例に、ワークシート関数の特徴と使い方について考えてみる。
この問題は初心者にループ構造を使わせるというのがテーマだろうが、残念ながらワークシート関数ではループができない。そのため、ループに代わる何種類かの手法を、ワークシートで実装してみる。
①まずは原始的にやってみる
そもそもExcelとは表計算ソフトである。表計算は、縦や横にセル参照しながら数式で計算し、計算結果をそのまま同じ表に出力する、というのが基本的な使い方になる。まずはプログラミング的な考え方を忘れ、セル参照と数式の概念を知ったばかりのExcel初心者でも解ける方法でやってみる。
解法例
str
の列が主な処理となる。セルの相対参照と数式のコピーを用いて、自らの上のセルに対して1つだけ"*"を追加する。idx
が1増えるごとに1行を使って順次処理し、指定されたN
に対応する結果が最終出力となる。初期値については最初の行を空白セルとすることで対応した。また、指定されたN
になったら処理をやめてその結果を返すことも必要になるが、今回は人力で済ませた。原始的な方法だが、初めてExcelを触ったときはこんなものでも感激していたのではなかろうか。
考察
さて、この原始的な解法だが、図らずも普通のループを使う解法と同じ構造になっている。
Sub N倍の文字列(N As Long) '普通のループを使う解法(VBA)
Dim idx As Long
Dim str As String
str = ""
For idx = 1 To N
str = str & "*"
Next idx
Debug.Print str
End Sub
つまり、
- インデックス
idx
と途中結果を格納する文字列str
がある - 初期値""に対し1ステップ(1行)ずつ処理をする
- 処理は前のステップ(上の行)の
str
に対して"*"
を結合する -
idx
が指定値N
に達したらその時のstr
を出力する
という構造である。
ここで気づくさらに重要な点は、
- プログラミングにおける変数は、ワークシートにおけるセルや列に対応する
ということである。細かい話はともかく、変数の特長である「値に名前を付けて可読性を上げる」「値を再利用できる」「修正時の変更箇所を減らして保守性を上げる」という点は、セルや列でも全く同じである。なお、Excelワークシート数式は関数型言語に分類されるが、その大きな特徴として「変数への再代入はできない(i = i + 1
ができない)」がある。Excelをベースに考えて、変数=セルだと思えば、再代入できないのも当たり前に受け入れられる。
この手法のまとめと課題
ワークシートではループはできないが、シートの1行を1ステップとして使えば同等に処理ができるという知見を得た。しかしながら、これでは問題を1つ解くのにシートを1枚使ってしまい、あまりに非現実的である。よって、次は1行でこの処理を実装したい。
②1行でやる
結論から言うと、REDUCE
関数を使うことで上記と同じ考えのまま1行で実装できる。
事前説明
REDUCE
関数の挙動を説明するため、まずはSCAN
関数で上記手法を再現してみる。
最初にSEQUENCE
関数を用いてインデックスの配列を作っておく。
配列は新しい機能(とも最早言えない?)スピルにより複数のセルに返される。また、名前の定義を使ってA2セルにN
と名前を付けて可読性を上げている。
つづいて、SCAN
関数でstr
列を作る。
構文がやや面倒だが、要するにさっきの1行を1ステップとしたループを回しているようなものである。ここで登場するLAMBDA
関数は関数を定義する関数であり、SCAN
関数はその関数に配列の要素を1つずつ渡して処理させる(ただし今回は配列の要素数以外の情報は用いていない)。
解法例
本命のREDUCE
関数はSCAN
関数と構文が同じだが、途中経過は返さず最終結果だけを返す。言語によってはFOLD関数と呼ばれるらしい。
=LET(
idx, SEQUENCE(N),
REDUCE("", idx, LAMBDA(acc,elm, acc & "*"))
)
1行縛りによりインデックスの配列をシートに返せないので、LET
関数を使って数式内で変数を定義して、そこに配列を格納している(この場合はLET
を使わなくても書けるが)。やっていることは先の2つの手法と同じだが、だいぶスマートになった。
この手法のまとめ
REDUCE
関数を使うと、ループや原始的な手法と同じ考えのまま1行で実装できる。
③LAMBDA関数の再帰でやる
LAMBDA関数
SCAN
関数のおまけのように書いてしまったLAMBDA
関数だが、機能的にはこっちが目玉である。この関数の導入によってExcelワークシート数式がチューリング完全になったのだから、Excel史上最も重要な関数の1つと言っていいだろう。LAMBDA
の概念は関数型言語から来ているらしいが、最近ではいろいろな言語でも導入されているらしいので、馴染みある人も多いかもしれない。
再帰を使う
さて、関数型言語の特徴として、LAMBDA
で関数を定義し、再帰を使うことで、ループと同様の処理ができるらしい。ということでやってみる。
NFOLDSTRING
という自作の関数を名前の定義に登録した。ぱっと見はネイティブワークシート関数と変わらずスマートに見える。
関数の中身はこんな感じ。
=LAMBDA(N,
IF(N = 0, "", NFOLDSTRING(N - 1) & "*")
)
関数の定義を分かりやすく言うなら
NFOLDSTRING(N) = NFOLDSTRING(N - 1) & "*"
ただしNFOLDSTRING(0) = ""
という感じである。
これまでと大きく違うのは、ループでは
「インデックス1からスタートして、Nまで処理したら終わる」だったのに対し、再帰では
「インデックスNからスタートして、1まで処理したら終わる」という書き方になっている点である
(動作としては、Nからスタートして1まで順に自身を呼び出した後、1からスタートして順に値を評価していき、Nに到達したら終わる、という往復運動になる)。
今回の問題ではどちらを使っても大差ないが、もともと再帰的な性質を持つ問題であればこの書き方のメリットがより活きてくることだろう。
この手法のまとめ
問題の種類によっては再帰も重要な選択肢になる。
④既存のワークシート関数でやる
実務ではまず使わないREPT
関数である。本当はもっと便利な関数を紹介したかった……
まとめ
- いくつかの重要なプログラミングの概念は、ワークシート関数にも当てはまる
- ワークシート関数でも、問題によってはループと同じ考えのまま実装できる
- 再帰も問題の種類によっては強力に使える
- ワークシート関数は便利
終わりに
本当はここまでが前置きで、もう少し複雑な問題について書きたかったのだが、想定より長くなってしまったので今回はここで終わりにする。ワークシート数式とプログラミングの関係については、どこかで本腰を入れてまとまった記事を書こうと思う。