javascriptの論理和演算子(||)とオブジェクトの存在しないキーへのアクセスの仕様について

javascriptの論理和演算子(||)とオブジェクトの存在しないキーへのアクセスの仕様について

背景

leetcodeのある問題のjavascriptによる解説コードでしらない文法を発見した。

解説より引用

var lengthOfLongestSubstring = function(s) {
    let maxLength = 0;
    let left = 0;
    let count = {};

    for (let right = 0; right < s.length; right++) {
        let c = s[right];
        count[c] = (count[c] || 0) + 1;
        
        while (count[c] > 1) {
            count[s[left]] -= 1;
            left++;
        }
        
        maxLength = Math.max(maxLength, right - left + 1);
    }
    
    return maxLength;    
};

何をするのか

count[c] = (count[c] || 0) + 1;

がどのような動作をするのかわからなかったので調査した。

調査

count[c]

MDNドキュメントより

オブジェクトに存在しないプロパティの値は undefined です(null ではありません)。

元解説コードより、count[c]の値はundefinedか数値。

count[c] || 0

MDNドキュメントより

x || y, x が true に変換できる場合は x を返し、それ以外の場合は y を返します。

同じく引用より

false に変換されうる式の例を示します。
null
NaN
0
空文字列 ("" または '' または ``)
undefined

count[c]が0かundefinedのときは0を返し、そうでないときはcount[c]を返す。

count[c] = (count[c] || 0) + 1;

count[c]が0や未定義のときは1を代入し、それ以外のときは+1したものを代入する。つまり、

if (count[c]) {
    count[c]++;
} else {
    count[c] = 1;
}

と同じである。

感想

javascriptの仕様についてより深く知ることができた。また、これらの表記はわかりづらいのでプロダクトでは使わなさそうだと思った。

Golangのfilepathパッケージを読む (2)

目標

Base()アルゴリズムを理解する。

ドキュメント

Base returns the last element of path. Trailing path separators are removed before extracting the last element. If the path is empty, Base returns ".". If the path consists entirely of separators, Base returns a single separator.

パスの最後の要素を返す。引数が空文字のときは"."を返し、区切り文字のみのときは1つだけの区切り文字を返す。

リーディング

まずはAPIを見る

func Base(path string) string {
    return filepathlite.Base(path)
}

filepathliteってなんだろうか。"internal/filepathlite"がimportされていたので見てみる。

filepathliteのBase()を見る

// Base is filepath.Base.
func Base(path string) string {
    if path == "" {
        return "."
    }
    // Strip trailing slashes.
    for len(path) > 0 && IsPathSeparator(path[len(path)-1]) {
        path = path[0 : len(path)-1]
    }
    // Throw away volume name
    path = path[len(VolumeName(path)):]
    // Find the last element
    i := len(path) - 1
    for i >= 0 && !IsPathSeparator(path[i]) {
        i--
    }
    if i >= 0 {
        path = path[i+1:]
    }
    // If empty now, it had only slashes.
    if path == "" {
        return string(Separator)
    }
    return path
}

最初に、引数pathが空である場合は"."を返す。つぎにpathの末尾の区切り文字(sepとする)を除き、pathの後ろから順番にsepとなる文字を探して最初からsepまでの文字を除く。このアルゴリズムでpathの最後の要素を返すという仕様を満たす。さらにpathが空であれば1つのsepを返すことで、pathがsepのみを含むときに1つのsepを返すという仕様を満たす。 なぜfilepathではなくfilepathliteで実装されているのだろうか。

filepathliteで実装されている理由の予想

ソースファイルのコメント

// Package filepathlite implements a subset of path/filepath,
// only using packages which may be imported by "os".

filepathlite/path.goの冒頭コメントによれば、"os"パッケージにインポートされるパッケージのみを使っているとのこと。

コミットログ1

all: rename internal/safefilepath to internal/filepathlite

The safefilepath package was originally added to contain the FromFS function. We subsequently added FromFS to path/filepath as Localize. The safefilepath package now exists only to permit the os package to import Localize.

Rename safefilepath to filepathlite to better indicate that it's a low-dependency version of filepath.

出典: golangのコミットログ 要約: safefilepathパッケージはosパッケージがLocalizeをインポートするためだけに存在していたが、filepathパッケージにもLocalizeが追加されているのでsafefilepathがfilepathの低依存バージョンであることをより明確に示すために名前を変更する。

コミットログ2

path/filepath, internal/filepathlite: move parts of filepath to filepathlite

The path/filepath package needs to depend on the os package to implement Abs, Walk, and other functions. Move the implementation of purely lexical functions from path/filepath into internal/filepathlite, so they can be used by os and other low-level packages.

出典: golangのコミットログ 要約: filepathはosパッケージに依存しているAPIがあるが、filepathの字句解析的な関数をfilepathliteに移動することで、osパッケージやその他の低レベルパッケージからインポートできるようにする。

理由の考察

osからfilepathパッケージをインポートするとos->filepath->osという循環依存がありできない。filepathのosに依存しないパッケージを別のパッケージに移動することで、osから元filepathのAPIを使えるようにするため。

テストを見る

次回やる。

まとめ

filepath.Base()のアルゴリズムについて理解した。そしてfilepathliteの存在理由についても知ることができ、そこから多くのことを学べたと思う。

まず学んだのは、GitHubHistoryでコミットログを遡るリサーチ技術。コードの背景を理解するための強力な方法を知ることができた。

そして、コミットメッセージの書き方。具体的な背景や変更の意図が丁寧に説明されていて、とにかく分かりやすかった。自分は(個人開発という言い訳もあり)いつも1行しか書いていないので、見習うべきだと感じた。

最後に、循環依存の解消方法。関数を何らかの基準でグループ分けし、別パッケージにまとめるという具体的なアプローチを知れた。

「プログラマー脳」 Chapter8の感想

プログラマー脳」Chapter8 「よりよい命名を行う方法」の感想

命名の考え方として、「文法的な一貫性」と「コードベース全体の一貫性」という2つの視点が提示されていた。文法的に一貫した命名は人間がその名前を見て理解する際のチャンク化を助ける。コードベースを通して一貫した命名は認知的負荷を下げる。文法的に一貫した命名というのは、例えばpage_counterをpge_ctrにしないといったようなもので、これに関しては実践しているつもりである。コードを通して一貫した命名については、あまり考えたことがなかった。また、初期の命名の慣習は永続的な影響を与えると記されており、78のコードにおける186のバージョンでの識別子の名前の質に関する調査が紹介されている。初期の命名の慣習が残り続けるというのは、個人開発のような小規模プロジェクトでも実感していたので実際に調査がされているのは興味深かった。そして、よりよい変数名のための3ステップモデルというのも紹介されていて、ステップ1の「名前に含めるべき概念を選択する」というのが一番重要で難しいと思った。

フェイテルソン氏によれば、何を名前に含めるべきか決める際に最も重要視すべきことは、名前の意図、つまりそのオブジェクトがどのような情報を保持し、何に使われるかということです。

とあり、命名の際にはオブジェクトの使われ方についても考えようと思う。

Golangのfilepathパッケージを読む

なぜ読もうと思ったのか

巨人の肩に立ちたいのと、イケていてかっこいいから。

目標

まずは一つの関数の処理を理解しようと思う。今回はfilepath.Absを選んだ。アルゴリズムを説明することを目標にする。

ドキュメント

Abs returns an absolute representation of path. If the path is not absolute it will be joined with the current working directory to turn it into an absolute path. The absolute path name for a given file is not guaranteed to be unique. Abs calls Clean on the result.

与えられたパスから絶対パスをもとめる。The absolute path name for a given file is not guaranteed to be unique. (google翻訳: 特定のファイルの絶対パス名が一意であることは保証されません。)とあり、シンボリックリンクなどの場合を指しているのだろう。アルゴリズムはドキュメントにあるように、パスのディレクトリとつなげているだけなことが予想される。

コードリーディング

ラップを発見

func Abs(path string) (string, error) {
    return abs(path)
}

path_plan9.go, path_unix.go, path_windows.goのそれぞれにabs()が定義されていた。OSの種類に関わらず、統一インターフェースを提供するためにラップしている。だが、Interface型を使わずにどうやってOSによって処理を分けているのだろうか。ビルドのときにパッケージに含めるファイルを決めているらしい。https://pkg.go.dev/cmd/go#hdr-Build_constraints

unixAbsの実装

plan9unixunixAbsを使って、うまく再利用している。Abs()のすぐ近くに定義されていたので見つけやすかった。

func unixAbs(path string) (string, error) {
    if IsAbs(path) {
        return Clean(path), nil
    }
    wd, err := os.Getwd()
    if err != nil {
        return "", err
    }
    return Join(wd, path), nil
}

ドキュメントに書いてある通りのアルゴリズムである。絶対パスならそのまま返し、そうでないならばディレクトリとつなげるだけ。

abs() in windows

func abs(path string) (string, error) {
    if path == "" {
        // syscall.FullPath returns an error on empty path, because it's not a valid path.
        // To implement Abs behavior of returning working directory on empty string input,
        // special-case empty path by changing it to "." path. See golang.org/issue/24441.
        path = "."
    }
    fullPath, err := syscall.FullPath(path)
    if err != nil {
        return "", err
    }
    return Clean(fullPath), nil
}

コメントにあるように、windowsのFullPathシステムコールでは空パスを入力するとエラーが返ってくるが、Absの仕様ではworking directoryを返したいので入力パスが空の場合は"."にする。処理をしている理由や背景が詳しく述べられていてとても分かりやすいコメントだった。私はifの前にこのようなコメントを書いていたのだが、ifの中の方が良いのだろうか。また、ifのカッコを閉じた後に空行を書いてないことが多い気がする。私はなんとなく空行を書いていたが、むしろ分かりづらくしていたかも。

後の処理はFullPathシステムコールを呼ぶだけ。

なぜwindowsではsyscallを使うのか

unixAbs()の中でOSに依存しない汎用的な関数をつかっていそうに見えたので、windowsでunixAbs()を使えない理由が分からない。調べたら、windowsではパスのルートはドライブであるかららしいが、詳しくはよくわからない。

まとめ

アルゴリズムが抽象化されているapiのおかげでとても分かりやすかった。コメントもそのコードを書いている背景や理由などを詳しく説明していてお手本のようであった。

「プログラマー脳」chapter10の感想

chapter10 「複雑な問題をよりよく解決するために」

この章では、問題解決においての人間の記憶システムの役割やそれらの記憶システムによる自動化が問題解決能力を向上させることなどについて述べていた。

特に面白かったのが学習関連負荷という概念である。これは認知的負荷の一種であるが、課題内在性負荷や課題外在性負荷などの認知的負荷が高すぎると学習がうまくできなくなるというのを初めて知った。問題解決のために脳が全力を出すことで、その問題解決法を長期記憶に保存できなくなってしまうそうだ。この本ではそれらに関する実験が紹介されている。

ある数学の問題を範例を見て解くグループAとグループBに分けた。その問題では当然のごとくグループAの方が成績が良かったのだが、驚くべきことにいくつかの異なる問題でテストしてみてもグループAの方が成績が良かったという。

私は問題を解決する際はヒントを見ずに自分の頭で考えることが一番学習できていると思っていたが、その考えは間違っているようだ。これらの事実は数学やコーディングなどにとても応用できるものであると思う。コードリーディングをしたほうが良いと言われるのも範例を知ることができ、それを用いた問題解決の際に脳に余裕ができ学習できるからなのかもしれない。

「良いコード悪いコードで学ぶ設計入門」の第9章 コレクションを読んだ感想

listやmapなどのコレクションについての実践的なメソッドが書いてあった。

1. コレクション操作を自前で実装しない

標準ライブラリにあるような操作も自分で作ってしまっていたかもしれないので、実装する前に生成AIにライブラリを聞いてみることを心がけようと思った。

2. 変更可能なインスタンス変数を返さない

コレクションのようなインスタンス変数を外部に変更可能な状態で返すようなメソッドを実装してしまうと、カプセル化の意味がなくなってしまうかららしい。 おそらく、インスタンスの変更はそれに実装されたメソッドによってのみ行われたほうがよいという考えによるものだろう。オブジェクト指向におけるカプセル化の定義や目的を知りたい。