ゆるふわ技術日誌

エンジニア見習いの悪戦苦闘日記

RegExp.prototype.execが偶数回しか動かない謎に遭遇したハナシ

先日、既存のコードのテストをしていたところ、正規表現のマッチの結果が実行するごとに変わるという謎の現象に遭遇しました。

適当な例を作るとこんな感じ。

const regExp= /\*\*(.+?)\*\*/g; // MarkdownのBoldの箇所を抽出する正規表現
const markdown = "こんにちは。今日の気温は**33度!**とめっちゃ暑いので熱中症に注意しましょう!"

regExp.exec(markdown);

意図としては、Markdownの文章中からBoldのところ(アスタリスク2つで始まり、間になんらかの文字があって、アスタリスク2つで終わる)を抽出するという感じ。(もちろん実際のMarkdownパーサを書くならもっと考慮する事はありますが。。。)

実際には、これがUtil系の処理の一部として組み込まれていて、場合によっては数回叩かれるケースがある、というようなコードでした。

さて、これを実行してみると以下のような結果になります。

regExp.exec(markdown)
/* ↓結果
[ '**33度!**',
  '33度!',
  index: 12,
  input: 'こんにちは。今日の気温は**33度!**とめっちゃ暑いので**熱中症に注意**しましょう!',
  groups: undefined ]
*/

Node.jsで実行していますが、ブラウザでも同じだとおもいます。

見事「33度!」という値が取り出せているので、これで良さそうな感じがします。

しかしこれと全く同じコードをもう一度実行するとこうなります。

regExp.exec('/hoge?fuga=thisisvalue')
null

なんということでしょう…。

ちなみにこれ以降、もう一度実行すると成功し、さらにもう一度実行すると再びnullになるという挙動を繰り返します。

MDNのexecメソッドの説明をみると以下のように書いています。

developer.mozilla.org

exec() メソッドは、指定された文字列内で一致するものの検索を実行します。結果の配列、または null を返します。

実はこれが罠で、結果の配列を返すというのは、一致する全ての結果を配列で返すという意味ではないのです…。

今回正規表現gフラグが付いているのに気づいたでしょうか…?

これはグローバルフラグと呼ばれるもので、文字列中から正規表現に一致する全ての結果を得たいときに使います。今回の例の場合、これをつけるとexecは実行するたびに、前回ヒットした位置より後ろで、正規表現にマッチする箇所がないかを探すという挙動をします。

探した結果、存在しなければnullを返します。偶数回しかうまくいかなかった原因は、1回目は文字列を先頭から探して、ヒットする箇所があったので値が返却されましたが、2回目の実行では、前回のヒット位置より後ろ側にヒットする箇所がなかったためnullが返ります。これが今回遭遇したバグの原因となっていました。。。

どう解決するか

解決アプローチは複数あります。

まず1つ目。RegExpクラスのインスタンスは前回のヒットした位置をlastIndexというプロパティで持っています。

developer.mozilla.org

lastIndex は、次のマッチの始まりの位置を示す、正規表現インスタンスの読み書き可能な整数値のプロパティです。

とあるので、この値をexecの実行前に0に戻してあげれば、毎回同じ結果を返す事になります。

ただし、通常gフラグを使うという事は、複数マッチする事が想定されているはずなので、毎回0に戻すと、先頭のマッチしか取得できなくなってしまいます。

そこで2つ目のアプローチは、String.prototype.matchを使うというパターン。

developer.mozilla.org

こちらはexecとは異なり、gフラグが付いている正規表現を使用すると、一致する全ての箇所が配列となって返ってきます。

"**hoge** fuga piyo **puyo**".match(/\*\*(.+?)\*\*/g);
// -> [ '**hoge**', '**puyo**' ]

ただしこちらは、マッチした位置のindexなどの詳しい情報や、カッコを使ったキャプチャ(正規表現内でカッコを使うとカッコ内の文字を取得できる)を使う事ができないため、必要に応じてマッチした文字列をさらに加工する必要が出る可能性があります。

……あ、もちろん先頭のマッチ結果しかいらないならgフラグを外す事でexecの結果は冪等になりますことよ!

要件次第というところかなとは思いますが、一つ言えるのはgフラグが付いている正規表現を使うときはロジックを一度確認してみたほうが良いということですね。

なるべく副作用のない関数を書きたいと心がけているのですが、まさかこんなところに罠が潜んでいるとは…。という感じでした。おわり。

ES6で追加されたSymbolって結局なんなのか

JavaScript基礎力強化を図っています。

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

この本を読み返しながら、知識のないところを学習するシリーズ。


ES6から新たにSymbolという型のデータが追加されました。

developer.mozilla.org

Symbol() 関数は常に一意の値を返します。symbol 値はオブジェクトのプロパティ識別子として使われます。symbol 型はこの目的のためだけに使われます。

とのことなので、実際にどんな感じで使われるのかをみていこうと思います。

シンボルの特徴

シンボルはSymbol()関数で作ることができます。

先頭が大文字なのでクラスっぽいですが、new Symbol()は使えず、 エラーになります。

Symbol()は呼ばれる度に一意な値を返します。つまり

Symbol() === Symbol() // -> false

となります。

使われ方 その①

MDNにもあるように、シンボルはオブジェクトのプロパティ識別子(=キー)として用いることができます。具体的には

const sym = Symbol()
const obj = {
  [sym]: 'hoge'
}

ということができます。([sym]としないと、symという名前のキーになってしまうので注意)

これが具体的に役に立つケースは、既存のオブジェクトやクラスに対して、プロパティを追加したりするパターン。

Symbol登場以前は既存のオブジェクトやクラスに対してプロパティを追加する際に、キーが競合する可能性があるのを回避する手段がありませんでしたが、Symbolは作成する度に一意な値となることが保証されているので、安全に行うことができます。

const TATEGAKI = Symbol();

// Stringのインスタンスメソッドに縦書き(1文字ごとに改行)にするメソッドを追加する
String.prototype[TATEGAKI] = function () {
    return Array.from(this).join('\n')
}

console.log("文字列"[TATEGAKI]())

これでESの仕様で縦書きにするメソッドが実装されても(笑)自分の作った縦書きメソッドと衝突する恐れはないので、実行系が新しくなった瞬間に既存のコードが壊れるといった現象を回避することができます。

使われ方 その②

シンボルにはSymbol()を使って自分で作る値だけではなく、事前に定義された値があります。

その中の一つに、Symbol.iteratorという値があります。この値は、イテレート可能なオブジェクト(StringやArray、Map、Setなど)がイテレータを持つときのキーとして使われています。

"Stringはイテレータを持っています"[Symbol.iterator]; // -> function
new Date()[Symbol.iterator]; // -> undefined

なぜシンボルをキーに使うかについては、以下の記事が大変わかりやすかったです。

qiita.com

ざっくりとまとめると、

  • JSには特殊メソッド(使用者が上書きしてはいけないメソッド)を定義する方法がない
  • いきなり、イテレータを返すメソッドの名前を仕様で決め打ちにしてしまうと、正常に動かないプログラムが発生する可能性がある
  • そこで定義済みのシンボルを用意し、その値をキーとして用いることで、安全に特殊メソッドを実装することにした。

という感じ。言語の仕様を考えるような人たちって、本当に頭いいんですね…と思いました。

近況報告

なんとびっくり、ぴったり80日ぶりの更新です。

お久しぶりです。

ブログを書く習慣をつけるのは大変だったのに、書かなくなったら本当に書かなくてびっくりしました。

前回の更新である、2月23日から今日に至るまでに何があったかざっくり書いておきます。技術的要素はないです。


2月〜3月末

4月から入社する会社で、一足先にインターンとして働いていました。

内容としてはReact NativeとTypeScriptを使ったアプリ開発、という感じ。前回の更新がRNだったのも、業務の一環でつまづいたところとかのうち、一般的に役に立ちそうなものを書いたものでした。

無難に楽しく、週5日で働いてました。

人とコミュニケーションとるのがそれほど得意でない自分にとって、このインターン期間は既存社員とのコミュニケーションをとる良い機会になりました。

4月

入社。新元号発表のその日に入社式でした。

とはいえインターンで通い慣れていたので、いつも通り…と思って出勤したら入社書類を一式家に置いてあったことに30分くらい電車乗ってから気がつきました。

その場で引き返して、自宅最寄り駅から自宅までタクシーを使って、なんとか5分遅刻くらいで到着しました。入社式に遅刻しなかったのでセーフ。


そこから1ヶ月くらいは研修の日々。

最初の3日間くらいはビジネス職・エンジニア職合同の研修を受け、残りはエンジニア・デザイナーの研修。

f:id:uutarou:20190514224623j:plain
こんな絶景の会議室で研修を受けてました

世の中の研修って結構つまらない、ひたすら退屈なイメージが強かった(こんな記事があったり)が、幸いにもうちの会社の研修は、必要最低限を抑えた実践的かつ、普遍的に使える知識を身に着ける研修で大変良かった。そこは良かった。

同期と仲良くなれずに悩んで、病みそうにもなったりしたけど、なんとか無事に研修を修了。

と、同時に世の中はGWに突入。私は横浜の実家を離れ一人暮らしを開始しました。

大学も実家から通っていたので完全に人生初の一人暮らしです。

f:id:uutarou:20190514225234j:plain
引越し初日の様子

自宅の車がミニバンだったことと、実家が横浜とそれほど遠くないことから、自力での引越しを決断。

結局GWは荷物作っては車で運ぶというのを繰り返していたら終わってしまいました。

新卒1年目のGWとか、一番やる気に満ちてる時だったのに、有意義な時間を過ごせなかったのは残念。

一人暮らし話は引っ越して1ヶ月経ったくらいで書きたいな。ふとした瞬間(今とか)に死ぬほどさみしくなるけど、今のところはとっても楽しんでます。

5月

一人暮らし & 本格的な業務開始。

業務は結局インターン時代と同じチームに配属され、引き続きReact Nativeを使ったアプリ開発をやっています。

リリースからはすでに数年経っているものの、「とにかく機能開発を優先する」というフェーズからつい最近脱したアプリなので、手の入れどころ、リファクタリングのやりがいがあるプロジェクトだと個人的には感じています。

コード規模もまぁまぁ。利用者数も(正確な数字は知らないけど)下手すりゃ万単位で居る感じ。

JavaScript→TypeScriptの基礎力をこの辺りで一回ちゃんとつけておきたいと感じています。


一人暮らしの方はというと、やっと落ち着いてきて、生活のリズムもできてきたかなと思っているところです。

料理を作って写真を撮るということをなぜか趣味にしています。

f:id:uutarou:20190514230154j:plain
同期にめっちゃ好評だったやつ
f:id:uutarou:20190514230216j:plain
CGみたいと言われた朝飯
f:id:uutarou:20190514230239j:plain
with オライリーな朝食

こんな感じ?

今の所9割くらいの晩飯は自炊してます。クラシルには頭が上がらない。

あと、一人暮らしになって変わったことといえば、家のネット回線がとっても速くなりました。(実家はADSLだった)

f:id:uutarou:20190514230512p:plain

マンションタイプの光の割には速いと思うんですよね。

せっかく高い金払って光引いたのでちゃんと活用してお勉強しようと思います。

まとめ

まとめもクソもない。

せっかく家に課金したので、この環境(出社時間が超短い/インターネットが引かれている静かな部屋が24時間使い放題)を活かして圧倒的成長を遂げたいです。

今日は眠いので寝ようかしらね。

【ReactNative】AndroidとiOSではキーボードが開いた時の挙動が違う

※タイトルにRNとかいてますが、おそらくRNに関係ない話だと思います。多分。

ReactNativeを使ってて、キーボードが開いた時の挙動がOS間で違うのでは?と思って調べてみたらやっぱりそうだった、という話。

どっかに書いてるかな、と思ったけど見つけられなかったのでここに書き残す。

結論 どう違うのか

なにがどう違うかというのをアニメGIFで撮ったので貼ります。

(追記: いらんとは思うけどコードあげました。 GitHub - uutarou10/rn-view-height-test )

https://files-uploader.xzy.pw/upload/20190223001844_3577626371.gif

(width/heightのラベルはそれぞれが100%に指定してある親Viewの値です)

キーボードの分、Viewが縮むのがAndroidの挙動で、そうではないのがiOSの挙動です。

なので、キーボードが開いた時にキーボードの下に要素が隠れないようにするとか、そういうものを作ろうとするとちょっと工夫する必要が出てきます。

でも…

Androidのキーボードが開いた時の挙動はAndroidManifest.xmlというファイルにあるandroid:windowSoftInputModeという値で設定されているようです。(この辺、Android開発者ではないのでかなり怪しめです。)

react-native-cliで作ったアプリは、この値が最初adjustResizeになっています。(ちなみにAndroidManifest.xmlはRNの場合、android/app/src/main/AndroidManifest.xmlにあります)

developer.android.com

こちらのページに書いていますが、この値をadjustPanにすると、フォーカス位置が隠れないように画面をズラしてくれます。

adjustPanに設定した場合の挙動はこうなります。

https://files-uploader.xzy.pw/upload/20190223003501_4343677778.gif

ただ、画面をズラしてくれると書いたように、Inputがキーボードが出る位置の下にある場合、フォーカスが当たると全体的に画面がズレます。

https://files-uploader.xzy.pw/upload/20190223004013_6675353965.gif

当然画面上にある要素は見えなくなってしまうので、実際に使うには注意が必要です。

実際、先ほどのAndroid公式のドキュメントには、

一般にこの方法はサイズ変更に比べると望ましくありません。ユーザーがソフト キーボードを閉じて、ウィンドウの隠れた部分を操作する必要が生じる可能性があるためです。

と書いています。

じゃあお前どうすりゃええねん、という話はまた調査して書こうかなと。

最近知ったTypeScriptのちょっとしたTipsとか

相変わらずインターンではTypeScriptを書いています。

最近は、ちょっと自分でも成長したなと思うのですが、バグの原因を追い求めてライブラリの中をウロウロしていたりします。

人生22年、ライブラリの中をのぞいてみよう、なんならバグ見つけてPRだそうとか考えたこともなかったですが、人間必要になればやるし、やってみると案外できるもんですね。(もちろん楽ではないけど、一個ずつ読み解くことくらいは、努力次第でできるな、と思った。)

雑談はさておき、そんなこんなの業務の中で知って、「へー」と思ったTypeScriptの諸々をメモ書き程度に書いておきます。


tscのメッセージを日本語にする

Compiler Options · TypeScript

まぁ公式に書いていることなんですけど。(この記事に出てくることは、私が知らなかったというだけで、全部公式に書いていることです。私の勉強不足です。)

オプションとして--locale jaを渡してあげると、いろいろ日本語になります。

たとえばこんなの

$ tsc --locale ja
バージョン 3.2.4
構文: tsc [オプション] [ファイル...]

例:  tsc hello.ts
    tsc --outFile file.js file.ts
    tsc @args.txt
    tsc --build tsconfig.json

オプション:
 -h, --help                                 このメッセージを表示します。
 -w, --watch                                入力ファイルを監視します。
 --pretty                                   色とコンテキストを使用してエラーとメッセージにスタイルを適用します (試験的)。
 --all                                      コンパイラ オプションをすべて表示します。
 -v, --version                              コンパイラのバージョンを表示します。
 --init                                     TypeScript プロジェクトを初期化して、tsconfig.json ファイルを作成します。
 -p ファイルまたはディレクトリ, --project ファイルまたはディレクトリ  構成ファイルか、'tsconfig.json' を含むフォルダーにパスが指定されたプロジェクトをコンパイルします。
 -b, --build                                最新でない場合は、1 つ以上のプロジェクトとその依存関係をビルドします
 -t バージョン, --target バージョン                   ECMAScript のターゲット バージョンを指定します: 'ES3' (既定)、'ES5'、'ES2015'、'ES2016'、'ES2017'、'ES2018'、'ESNEXT'。
 -m 種類, --module 種類                         モジュール コード生成を指定します: 'none'、'commonjs'、'amd'、'system'、'umd'、'es2015'、'ESNext'。
 --lib                                      コンパイルに含めるライブラリ ファイルを指定します。

例がぱっと用意できなかったですが、コンパイルエラーなどももちろん日本語になります。

英語がわからんとき、やってみると救われるかもしれない。

(TSのコンパイルエラー自体そんなに難しい英語は一切出てこないので、必要かと言われれば微妙ですが)

(変数名)!(Non-null assertion operator)

コード読んでいたら、hoge!(hogeは変数)みたいなのが出てきて、これはなんだ?と思ったという話。

!という時点で察しのいい人は分かりそうな気もしますが、これはコンパイラに対して、変数が強制的にnullにならないと伝えるためのオペレーターみたいです。(Non-null assertion operatorと言うらしい)

つまり、hoge変数の型がstring | undefinedだったとき、hoge!とするとhoge as stringとした時と同じ動きをすることができるようです。asなんちゃらと書くとどうしても長くなるので、これは使っていきたいテク。(本来はキャストなんぞ使いたくないが)

ここに書いてた。

TypeScript 2.0 · TypeScript

型と型を&でつなぐ(Intersection Types)

うまい例が思いつかないですが、こんな感じ。(公式の例をパクってシンプルにした)

interface Person {
  name: string;
  age: number;
}

interface Speakable {
  greet(): void;
}

const kanameRin: Person & Speakable = {
  name: '鹿目凛',
  age: 22,
  greet() {
    console.log('イラストも描けちゃうあなたの彼女♡');
  }
}

PersonSpeakableというインタフェースが宣言されていて、定数kanameRinPerson & Speakable型になっています。

右辺を見てもらえれば一目瞭然ですが、型名と型名を&でつなぐことで、型を組み合わせることができます。

(ちなみに、サンプルコードの元ネタはこちら。最近の推しです。)

ちなみに

interface A {
  hoge: string;
}

interface B {
  hoge: number;
}

const aAndB: A & B = {
  hoge: 'hoge' // ここでコンパイルエラーになる、hogeはstring & number型
};

こんな感じで、同名で別の方が存在すると、hogestirng & number型として扱われるみたいです。(なので上記の例はエラーになります。)

これ、string | numberとして扱ってくれた方がいいような気がしなくないけど、そうでもないのかしら。

あ、ちなみにこのような感じで、型を&でつなぐのをIntersection Typesというみたいで、以下に載ってます。

Advanced Types · TypeScript

インターンを始めて1週間が経った。

表題の通りなのだが、4月から就職する内定先で、入社に先立って、インターンを始めて約1週間が経過したので、今の気持ちをまとめておこうかな、というのがこの記事の主旨。お気持ちポエムなので、スルーしてほしい。

何をやっているか

どこまで書くと怒られが発生してしまうのかわからないので、適当にぼやかして書くと、TypeScriptで書かれたReact Nativeアプリの機能開発をしています。

とは言いつつ、1週間では環境構築→設計の理解→追加する機能の設計→見た目に関係のない部分の実装というところまでしか到達しなかったので、RN要素はほぼ無で、ひたすらTypeScriptと戯れていた感じの1週間でした。

1週間過ごしてみての所感

技術以外のところで結構課題を感じたので、とりあえず書いておく。

初日、出社したらきているはずのSlackの招待が来ていないというトラブルに見舞われた。おまけに、一緒のチームの人たちは朝からミーティングで席を外してしまうという…。

自分の席の周りに誰もいなくなってしまって、Slackの招待も来ていなくておどおどしてしまった。

結局、数分おどおどしたあと、なんとか顔見知りのエンジニアの人を捕まえて、指示を仰ぐことができたが、どうしてもっとサクッとできないかなーと、ちょっと自己嫌悪に陥った。

実際仕事を始めてみると、様々な機会でコミュニケーションをとる必要があることを知った。既存の実装のわからないところを開発メンバーに聞いたり、API仕様を聞くためにサーバーサイドのエンジニアのところに行ったり。

幸いなことに、関わる人は皆さんいい人で、私が新人であることも理解してくれているので、どんな小さなことでも聞いたら丁寧に教えていただけるので、1週間経ってやっと慣れてきた感じがあるが、それでも初めての人に声をかけるのは精神的に結構くる。

これから先、入社したらもっと多くの人と関わる機会があると思うし、チームの入れ替えとかも結構激しくやってるような話を聞いたりするので、コミュニケーション能力の強化は真っ先に今後の課題になった。

ひとまず、精神的にきつくてもガンガン話しかけるということを目標に週の後半は過ごしてみた。

仕様の確認とかも、トレーナーが代わりにやってくれていたのを、誰に聞けばいいかだけ聞いて自分で行くようにしたり、ディスプレイがなかったので、貸してほしいとCTO(!!)に言いに行ったり。

CTOに直接言いに行くのは、さすがに緊張してしまって、喋る言葉がフェードアウトするというコミュ障大爆発っぷりだった。

ちょっと与えた印象悪かったかもなぁ…とか考えちゃうけど、そんなことずっと考えてても仕方がないので、忘れることにする。

技術編

技術的なところでももちろん課題に感じたことはたくさんあった。

割としっかり触ってきたつもりのTypeScriptも、業務レベルになれば全然理解が足りてないし、でかいコードの動きを追うのは本当に厳しいし、ついでに環境構築でも結構つまづいたし。

そんな感じのレベルでも、呆れず懇切丁寧に教えてくれるトレーナーをはじめとする周りの皆さんには頭が上がらないし、現状は時給泥棒状態なので、早く給料に見合った働きをできるレベルまで成長していきたいです。

本当は、技術以外編と同じくらいの分量、技術編も書いていたんだけど、技術は(少なくとも自分の現在のレベルでは)やればやるだけ伸びる状態にあると思うので、あえて書かないこととしてみた。

お尻に火のついた僕は強い。

結局何が言いたかったのか

文章書くの苦手なのに、オチを考えずに書くので、いつも駄文が出来上がる。この辺も今後の課題としていきたい。

ここまで読んでしまった方がいたら、ごめんなさい。言ってくれたらお詫びにコーヒーおごります。

結局何が言いたかったのか僕もわかりませんが、月曜から始まる次の1週間は、

  • 頑張ってコミュニケーション取っていく。コミュ力の底上げにチャレンジする。
    • 頑張らずにコミュニケーション取れるようになるのは頑張った後なのではないかなという仮説
  • 自分の限界を決めず、(少なくとも今はまだ限界が見える段階じゃない)貪欲に学ぶ。フロントエンド書いているけど、必要ならサーバーサイドも読むとか、今開発している機能以外の部分についても、理解を進める。

とかこん感じでチャレンジしていこうかな。

月曜が楽しみです。

実は土曜日は1週間の疲れで丸一日死んでいたので、日曜は月曜に備えて勉強したりしたい。(インターン生はコードを持ち帰れないので、きついなーとか思っていたけど、実際の実装という超具体的なものではなく、もっと抽象的なところを学べというメッセージなのかもとか勝手に思った。多分違うけど)

【iOS12】iOS Simulatorにペーストするのがうまくいかない時

久しぶりの更新です。ずっと書いてないと書く習慣が失われそうなので、ちっちゃなネタでもどんどん投下していきます。

昨日から始めたバイト?インターン?での知見。


Macでコピーした文字列などをiOS端末上で貼り付ける方法。iOSはv12、Xcodeはv10.1です。古いバージョンだと違うかも。

通常はCmd + Vで通常通り貼り付けられます。なんの問題もないです。

ところが、開発していたら急に効かなくなってしまいました。

長いURL手打ちするのかよ〜と思っていたのですが、

f:id:uutarou:20190213223650p:plain

Edit > Automatically Sync Pasteboardのチェックが外れていました。

この状態でコピペを使う方法は2通りあって、

  • Automatically Sync Pasteboardのチェックを入れて再度Mac側でコピーする
  • Send Pasteboardをクリックして、iOS内で貼り付けの操作を行う

です。

古い情報に惑わされたりして地味に時間を潰したので書いてみました。