LoginSignup
2
0

More than 1 year has passed since last update.

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で表します。
まず桁数を調べるために、getDigitsnumberbaseの何乗までに収まるかを割り出します。乗数がmultiplerです。
そして、changeBaseで上の桁からbasemultipler乗で割っては余りを出し、その余りをmultipler - 1乗で割って、、、という再帰計算をして、最終的にmultiplerが0になったら桁ごとの配列digitsListを返しています。

うん、ひとまず動きますね。
26進数をアルファベットにするところは、まあなんとかなりそうだし、またあとで考えます。
しかし、もっさいコードだな。

もっとスマートにしたい

セルフレビューをします。

  • getDigitsも再帰処理にしたい。値を変更していくとバグを生みやすくてよくないって噂に聞くから(後々知ることになる参照透過性のこと)。
  • 型を指定しないと危険ですよね。
  • getDigitsbaseChangedとは別で呼び出しているのがいけてない。
    =>baseChangedの引数のmultiplerdigitsListをoptionalにしてやって、初回呼び出しではnumberbaseだけ指定して、そこで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つ目の型宣言ドラゴンを倒すときに気付いた過ち

では、型を付与していこう。変数のnumberint型にしたいよね。
......ん?なんかvscodeが受け付けてくれてない気がする。

image.png

ぐぐってみたら、なるほど。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();

大きく変わった部分はchangeBasemultiplerのdefault引数をgetDigitsで割り出している点ですね。
元々は、初回に桁数を決めるためにgetDigitsを呼び出して、判明した桁数をchangeBaseに渡すという方法を取っていました。
それをchangeBasemultiplerのdefault引数をgetDigitsにしたことで、入力はchangeBaseに対してnumberbaseだけで済むようになりました。
これが思いついた瞬間は自分が天才なんじゃないかと思いましたが、多分これ漸化式とか一般項とかそういうのが分かってる人からすると超普通のことなんでしょうね。

最後のドラゴン「配列への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以上の進数を扱うにはこれがベストかと。

おわりに

一番最初のとりあえず動くコードができた時点で優秀な後輩氏に「ひとまず書けましたけど、見ます?」と声を掛けたら、なんとすでに完成させたとのことでした。
全く可愛げがないですね。

週明け(この記事がアドベントカレンダーで公開される日)に、お互いが書いたものを見せ合うので、楽しみです。

参考

MDN
サバイバルTypeScript
TypeScript Deep Dive 日本語版

2
0
12

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0