typescriptで参照透過性を意識してみた
これまでは動けばOKの考えだったので、関数型プログラミングね、何かあるよね程度でした。
今回、typescriptを書く機会があり、せっかくなので、動けばOKコード => 参照透過性を意識したコードにリファクタリングしてみました。
ちなみに参照透過性というワードも、今回この投稿を書いていくうちに知りました。
なお、最初におことわりしますが、私はjavascriptとかtypescriptは初心者に産毛が生えた程度です。
先輩諸兄がここから得られるものはないと思いますが、懐かしい気持ちになっていただければ幸いです。
あと、参照透過性についても誤解をしている可能性はあり、そこは突っ込んでください。
優秀な後輩が珍しく困っていた
「typescriptでExcelを扱うにあたって、列のアドレスを数字 => アルファベットにしたいんだけど、どうやればいいのか」
普段私がアドバイスをもらうことが多い立場なので、ここはなんとか役に立ちたい!
しかも、同じようなことを私も過去にやったことがありました。
ただ、その時はExcelで完結する処理だからVBAで書いたし、そのコードもどこへやら状態なので、
「再帰させたことは覚えている。あと、大きい桁から処理した。」
という、おぼろげなことしか言えませんでした。
よし、自分も作ってみよう。
ひとまず1時間くらいで、一応動くものはできました。
この時点では参照透過性などは一切考慮してません。
function changeBase(number, base, multipler, digitsList){
const divResult = Math.floor(number / (base ** multipler))
const modResult = number % (base ** multipler)
digitsList.push(divResult)
multipler -= 1
if(multipler > -1){
return changeBase(modResult, base, multipler, digitsList)
}
return digitsList
}
function getDigits(number, base){
let digits = 0
while(number >= (base ** digits)){
digits += 1
}
return digits - 1
}
現状使い方はこんな感じ。
Excelの最終列XFDを10進数にした16384を26進数で表現してみます。
const main = () => {
const number = 16384
const base = 26
const multipler = getDigits(number, base)
const baseChanged = changeBase(number, base, multipler, [])
console.log(baseChanged)
// [ 24, 6, 4 ]
}
main()
指定された数number
を指定した進数base
で表します。
まず桁数を調べるために、getDigits
でnumber
がbase
の何乗までに収まるかを割り出します。乗数がmultipler
です。
そして、changeBase
で上の桁からbase
のmultipler
乗で割っては余りを出し、その余りをmultipler - 1
乗で割って、、、という再帰計算をして、最終的にmultipler
が0になったら桁ごとの配列digitsList
を返しています。
うん、ひとまず動きますね。
26進数をアルファベットにするところは、まあなんとかなりそうだし、またあとで考えます。
しかし、もっさいコードだな。
もっとスマートにしたい
セルフレビューをします。
-
getDigits
も再帰処理にしたい。値を変更していくとバグを生みやすくてよくないって噂に聞くから(後々知ることになる参照透過性のこと)。 - 型を指定しないと危険ですよね。
-
getDigits
をbaseChanged
とは別で呼び出しているのがいけてない。
=>baseChanged
の引数のmultipler
とdigitsList
をoptionalにしてやって、初回呼び出しではnumber
とbase
だけ指定して、そこでgetDigits
を実行する形にすればいいのか? -
digitsList
にpushしていくのも、どうにかならないものか。これもバグを生みやすくてよくないって噂に聞くから。でも、これは現時点でno idea。
とりあえず、3匹目のドラゴンまでは何とか倒せそう。
桁数取得を再帰処理してみました
これは、number > (base ** multipler)
がTrue
になるまでmultipler
を足していくという再帰処理をすればいいんだな。
てことは、三項演算子の出番ですね。
function getDigits(number, base, multipler){
return(number >= (base ** multipler) ? getDigits(number, base, multipler + 1) : multipler - 1)
}
なんか、めっちゃいい感じになった気がする!
最初return
をそれぞれの条件の側に入れて、なんかうまくいかないなーなんて思ってましたが、よく考えればreturn
するのは三項演算子の結果で良いわけですもんね。
こういうところはやっぱり経験のなさが如実に出ますね。
2つ目の型宣言ドラゴンを倒すときに気付いた過ち
では、型を付与していこう。変数のnumber
はint
型にしたいよね。
......ん?なんかvscodeが受け付けてくれてない気がする。
ぐぐってみたら、なるほど。int
ではなくnumber
型を指定するのか。
どういう弊害があるのかは分かってないけど、なんとなくnumber: number
は避けたいから変数名をnum
にでも変えることとします。
ここでの変化は大したことないのでcodeは省略。
初回必要ない引数をoptionalにしてみました
const main = () => {
const number = 16384
const base = 26
const baseChanged = changeBase(number, base)
console.log(baseChanged)
// [ 24, 6, 4 ]
}
function changeBase(num:number, base:number, multipler:number = getDigits(num, base), digitsList:number[] = []){
const divResult = Math.floor(num / (base ** multipler))
const modResult = num % (base ** multipler)
digitsList.push(divResult)
if(multipler > 0){
multipler -= 1
return changeBase(modResult, base, multipler, digitsList)
}
return digitsList
}
function getDigits(num:number, base:number, multipler:number = 0){
return(num >= (base ** multipler) ? getDigits(num, base, multipler + 1) : multipler - 1)
}
main();
大きく変わった部分はchangeBase
のmultipler
のdefault引数をgetDigits
で割り出している点ですね。
元々は、初回に桁数を決めるためにgetDigits
を呼び出して、判明した桁数をchangeBase
に渡すという方法を取っていました。
それをchangeBase
のmultipler
のdefault引数をgetDigits
にしたことで、入力はchangeBase
に対してnumber
とbase
だけで済むようになりました。
これが思いついた瞬間は自分が天才なんじゃないかと思いましたが、多分これ漸化式とか一般項とかそういうのが分かってる人からすると超普通のことなんでしょうね。
最後のドラゴン「配列へのpush」
こんな感じになりました。
function changeBase(num:number, base:number, multipler:number = getDigits(num, base), digitsList:number[] = []){
const divResult = Math.floor(num / (base ** multipler))
const modResult = num % (base ** multipler)
const currentDigitsList = digitsList.concat([divResult])
if(multipler > 0){
multipler -= 1
return changeBase(modResult, base, multipler, currentDigitsList)
}
return currentDigitsList
}
変更点は、再帰処理で都度やってくるdigitsList
とその回で判明したdivResult
を単純にconcat
して新たな配列currentDigitsList
を作成してます。
そして、currentDigitsList
を次の回のdigitsList
に渡してやると。
これなら、pythonでいうtupleみたいなimmutableな型でも使えるわけで、参照が透明といえるのではないでしょうか?(ちょっと自信がないので、間違っていたらツッコミお願いします!)
当初は、その回で判明した桁の数を配列にpushして、その配列を再帰処理に渡して、ということをやっていて、危険性は分かるんだけどそれ以外にやり方なんてある?ってくらいに考えが固まってました。
一回ライブして、終電にギリギリ乗って、睡眠して、起きてやっと思いつきました。
悩んだときに毎回このプロセスを経ていては体が持たないですね。
アルファベットに直すところも考えました
asciiコードがどのくらいの不変なのかは分かってませんが、まあアルファベット順にコードが振られなくなるということはそうそうないかと思い、こんな感じにしました。
function num2char(digitsList:number[]){
return(digitsList.map((c: number) => (String.fromCharCode(c + ("A".charCodeAt(0) - 1)))).join(""))
}
26進数の桁ごとの配列を引数として受け取ったら、それぞれの桁の数に"A"のascii code -1を足しています。
const main = () => {
const num = 16384
const base = 26
const baseChanged = changeBase(num, base)
console.log(baseChanged)
// [ 24, 6, 4 ]
console.log(num2char(baseChanged))
// XFD
}
IPアドレスも試してみよう
曲がりなりにもインフラエンジニアの端くれなので、IPアドレスで試してみます。
今回できたコード全部載せつつ。
const main = () => {
const ipAdderss = "192.168.0.1"
const ipBin = ipAdderss.split(".").map((x: string) => (changeBase(Number(x), 2)))
console.log(ipBin)
// [[1, 1, 0, 0, 0, 0, 0, 0],[1, 0, 1, 0, 1, 0, 0, 0],[ 0 ],[ 1 ]]
}
function changeBase(num:number, base:number, multipler:number = getDigits(num, base), digitsList:number[] = []){
const divResult = Math.floor(num / (base ** multipler))
const modResult = num % (base ** multipler)
const currentDigitsList = digitsList.concat([divResult])
if(multipler > 0){
multipler -= 1
return changeBase(modResult, base, multipler, currentDigitsList)
}
return currentDigitsList
}
function getDigits(num:number, base:number, multipler:number = 0){
return(num >= (base ** multipler) ? getDigits(num, base, multipler + 1) : multipler - 1)
}
// この例では以下は使ってません
function num2char(digitsList:number[]){
return(digitsList.map((c: number) => (String.fromCharCode(c + ("A".charCodeAt(0) - 1)))).join(""))
}
main()
インフラエンジニアですが端くれなので、[[1, 1, 0, 0, 0, 0, 0, 0],[1, 0, 1, 0, 1, 0, 0, 0],[ 0 ],[ 1 ]]
を見て即時に計算はできません。
ここで調べてみたら、合ってました。
2進数は桁で区切られると中々可読性が落ちちゃいますが、10以上の進数を扱うにはこれがベストかと。
おわりに
一番最初のとりあえず動くコードができた時点で優秀な後輩氏に「ひとまず書けましたけど、見ます?」と声を掛けたら、なんとすでに完成させたとのことでした。
全く可愛げがないですね。
週明け(この記事がアドベントカレンダーで公開される日)に、お互いが書いたものを見せ合うので、楽しみです。