paizaの問題「N倍の文字列」を例に、Excelワークシート関数をプログラミング的に考えてみた

# この記事の概要
[paiza x Qiitaコラボキャンペーン](https://paiza.jp/pages/campaign/paiza-qiita)の問題「N倍の文字列」を、Excelワークシート関数を使って4通りの手法で解き、プログラミング的な観点を加えて考察した。

# これを書いたきっかけ
ここ数年で[Excelワークシート関数が超強化され、プログラミング言語と呼べるようになった](https://x.com/satyanadella/status/1359289655565291520?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1359289655565291520%7Ctwgr%5E39791a9cbee017e3a2d5cb47aafa09d420fff2fb%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fwww.publickey1.jp%2Fblog%2F21%2Fexcellambdaexcelceoexcel.html)のだが、その割に盛り上がりが少ない。ということで、[paiza x Qiitaコラボキャンペーン](https://paiza.jp/pages/campaign/paiza-qiita)の問題を使って、Excelのワークシート関数で実装例を紹介~~してみた~~する下準備としてワークシート関数について考えてみたら思ったより長くなったので、独立した記事にしてみた。
なお、筆者はプログラミング素人であり、誤り等が含まれている可能性がある。ご意見・ご指摘等は大歓迎である。

# ワークシート関数の特徴と使い方を、プログラミングと対比して考える（「N倍の文字列」を例に）
簡単そうな[N倍の文字列](https://paiza.jp/works/mondai/d_rank_skillcheck_archive/square)を例に、ワークシート関数の特徴と使い方について考えてみる。
この問題は初心者にループ構造を使わせるというのがテーマだろうが、残念ながら**ワークシート関数ではループができない**。そのため、ループに代わる何種類かの手法を、ワークシートで実装してみる。
    
## ①まずは原始的にやってみる
そもそもExcelとは表計算ソフトである。表計算は、縦や横に**セル参照**しながら**数式で計算**し、計算結果をそのまま同じ表に出力する、というのが基本的な使い方になる。まずはプログラミング的な考え方を忘れ、セル参照と数式の概念を知ったばかりのExcel初心者でも解ける方法でやってみる。

### 解法例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3869613/c8bf9666-6106-9229-3538-e615d5d748b6.png)
`str`の列が主な処理となる。セルの相対参照と数式のコピーを用いて、自らの上のセルに対して1つだけ"*"を追加する。`idx`が1増えるごとに1行を使って順次処理し、指定された`N`に対応する結果が最終出力となる。初期値については最初の行を空白セルとすることで対応した。また、指定された`N`になったら処理をやめてその結果を返すことも必要になるが、今回は人力で済ませた。原始的な方法だが、初めてExcelを触ったときはこんなものでも感激していたのではなかろうか。

### 考察
さて、この原始的な解法だが、図らずも普通のループを使う解法と同じ構造になっている。
```vb
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`関数を用いてインデックスの配列を作っておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3869613/925180e6-02a0-829d-96b7-ff4428d522c0.png)
配列は新しい機能（とも最早言えない？）**スピル**により複数のセルに返される。また、**名前の定義**を使ってA2セルに`N`と名前を付けて可読性を上げている。
つづいて、`SCAN`関数で`str`列を作る。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3869613/27a63b24-f95f-c3fc-656a-8d9552871662.png)
構文がやや面倒だが、要するにさっきの1行を1ステップとしたループを回しているようなものである。ここで登場する`LAMBDA`関数は**関数を定義**する関数であり、`SCAN`関数はその関数に配列の要素を1つずつ渡して処理させる（ただし今回は配列の要素数以外の情報は用いていない）。

### 解法例
本命の`REDUCE`関数は`SCAN`関数と構文が同じだが、途中経過は返さず最終結果だけを返す。言語によってはFOLD関数と呼ばれるらしい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3869613/f1e71e9c-cfce-b07a-4941-8d59902e5c34.png)

```
=LET(
    idx, SEQUENCE(N),
    REDUCE("", idx, LAMBDA(acc,elm, acc & "*"))
)
```
1行縛りによりインデックスの配列をシートに返せないので、`LET`関数を使って**数式内で変数を定義**して、そこに配列を格納している（この場合は`LET`を使わなくても書けるが）。やっていることは先の2つの手法と同じだが、だいぶスマートになった。

### この手法のまとめ
`REDUCE`関数を使うと、ループや原始的な手法と同じ考えのまま1行で実装できる。

## ③LAMBDA関数の再帰でやる
### LAMBDA関数
`SCAN`関数のおまけのように書いてしまった`LAMBDA`関数だが、機能的にはこっちが目玉である。この関数の導入によって[Excelワークシート数式がチューリング完全になった](https://www.publickey1.jp/blog/21/excellambdaexcelceoexcel.html)のだから、**Excel史上最も重要な関数の1つ**と言っていいだろう。`LAMBDA`の概念は関数型言語から来ているらしいが、最近ではいろいろな言語でも導入されているらしいので、馴染みある人も多いかもしれない。
### 再帰を使う
さて、関数型言語の特徴として、`LAMBDA`で関数を定義し、**再帰**を使うことで、ループと同様の処理ができるらしい。ということでやってみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3869613/d3a8507c-9b93-bcb8-a323-265e5797a82d.png)
`NFOLDSTRING`という**自作の関数を名前の定義に登録**した。ぱっと見はネイティブワークシート関数と変わらずスマートに見える。
関数の中身はこんな感じ。
```:NFOLDSTRING(N)
=LAMBDA(N,
    IF(N = 0, "", NFOLDSTRING(N - 1) & "*")
)
```
関数の定義を分かりやすく言うなら
`NFOLDSTRING(N) = NFOLDSTRING(N - 1) & "*"`ただし`NFOLDSTRING(0) = ""`
という感じである。
これまでと大きく違うのは、ループでは
「インデックス1からスタートして、Nまで処理したら終わる」だったのに対し、再帰では
「インデックスNからスタートして、1まで処理したら終わる」という書き方になっている点である
(動作としては、Nからスタートして1まで順に自身を呼び出した後、1からスタートして順に値を評価していき、Nに到達したら終わる、という往復運動になる)。
今回の問題ではどちらを使っても大差ないが、もともと再帰的な性質を持つ問題であればこの書き方のメリットがより活きてくることだろう。

### この手法のまとめ
問題の種類によっては再帰も重要な選択肢になる。

## ④既存のワークシート関数でやる
Excelワークシートには多くの関数が用意されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3869613/bda435b4-0614-5e5e-aed1-bb9d2817c3a7.png)

実務ではまず使わない`REPT`関数である。本当はもっと便利な関数を紹介したかった……

# まとめ
* いくつかの重要なプログラミングの概念は、ワークシート関数にも当てはまる
* ワークシート関数でも、問題によってはループと同じ考えのまま実装できる
* 再帰も問題の種類によっては強力に使える
* ワークシート関数は便利

# 終わりに
本当はここまでが前置きで、もう少し複雑な問題について書きたかったのだが、想定より長くなってしまったので今回はここで終わりにする。ワークシート数式とプログラミングの関係については、どこかで本腰を入れてまとまった記事を書こうと思う。