C++ の std::uniform_int_distribution は、乱数エンジンが生成する値より大きな整数を生成できる

タイトル通り。

https://wandbox.org/permlink/IQJOgUKdZ2k2KmMf

#include <utility>
#include <random>

struct my_random_generator {
    typedef std::mt19937::result_type result_type;
    static constexpr result_type min() noexcept { return 0; }
    static constexpr result_type max() noexcept { return 1; }

    template<class... Args>
    explicit my_random_generator(Args&&... args)
        : orig_(std::forward<Args>(args)...) {}

    template<class... Args>
    void seed(Args&&... args) {
        this->orig_.seed(std::forward<Args>(args)...);
    }

    result_type operator()() {
        return this->orig_() % 2;
    }

    void discard(unsigned long long z) {
        this->orig_.discard(z);
    }

 private:
    std::mt19937 orig_;
};

#include <iostream>

int main() {
    my_random_generator gen;
    std::uniform_int_distribution<> dist(1, 6);

    for (int i = 0; i < 10; ++i) {
        std::cout << dist(gen) << std::endl;
    }
}
1
6
4
5
6
3
1
6
4
2

上記コードで用いられている乱数生成エンジンは 0 か 1 かのみを返すエンジンだが、そのような極端なエンジンであっても、 std::uniform_int_distribution を用いれば、きちんと整数乱数を生成できる。


試しに乱数を生成するたびにログを出してみる:

https://wandbox.org/permlink/m2PUQEN3WnrQPvCI

#include <utility>
#include <random>
#include <iostream>

struct my_random_generator {
    typedef std::mt19937::result_type result_type;
    static constexpr result_type min() noexcept { return 0; }
    static constexpr result_type max() noexcept { return 1; }

    template<class... Args>
    explicit my_random_generator(Args&&... args)
        : orig_(std::forward<Args>(args)...) {}

    template<class... Args>
    void seed(Args&&... args) {
        this->orig_.seed(std::forward<Args>(args)...);
    }

    result_type operator()() {
        std::cout << "random number generated.\n";
        return this->orig_() % 2;
    }

    void discard(unsigned long long z) {
        this->orig_.discard(z);
    }

 private:
    std::mt19937 orig_;
};

int main() {
    my_random_generator gen;
    std::uniform_int_distribution<> dist(1, 6);

    for (int i = 0; i < 10; ++i) {
        std::cout << dist(gen) << std::endl;
    }
}
random number generated.
random number generated.
random number generated.
1
random number generated.
random number generated.
random number generated.
6
random number generated.
random number generated.
random number generated.
random number generated.
random number generated.
4
random number generated.
random number generated.
random number generated.
5
random number generated.
random number generated.
random number generated.
6
random number generated.
random number generated.
random number generated.
3
random number generated.
random number generated.
random number generated.
random number generated.
random number generated.
random number generated.
random number generated.
1
random number generated.
random number generated.
random number generated.
6
random number generated.
random number generated.
random number generated.
4
random number generated.
random number generated.
random number generated.
2

一回の整数乱数生成に際して複数回乱数エンジンが呼び出されていることが分かる。


参考:

cpplover.blogspot.com

Object.entriesの戻り値の型を厳密にするためにNegated typesが欲しい

Object.entries() - JavaScript | MDN

Object.entries は、オブジェクトに含まれる key と value の組み合わせを配列にして返してくれる関数で、 Javascript でコードを書いてるとお世話になることも多いと思います。
しかし、これを Typescript で使おうとすると、型が微妙になってしまうという問題点があります:

type Hoge = {
    a: string;
    b: number;
};

function f(x: Hoge) {
    const entries = Object.entries(x);
    // entries の型は [string, string | number][]
    for (const [key, value] of entries) {
        // 処理
    }
}

これを解決するために、以下のような手法を使うことができます:

zenn.dev

type Entries<T> = (keyof T extends infer U
    ? U extends keyof T
        ? [U, T[U]]
        : never
    : never)[];

function getEntries<T extends Record<string, unknown>>(obj: T): Entries<T> {
    return Object.entries(obj) as Entries<T>;
}

type Hoge = {
    a: string;
    b: number;
};

function f(x: Hoge) {
    const entries = getEntries(x);
    for (const [key, value] of entries) {
        if (key === 'a') {
            console.log(value.toUpperCase());
        } else if (key === 'b') {
            console.log(value + 1);
        } else {
            // value は never
            throw new Error('should not get here.');
        }
    }
}

これを使えば厳密に型付けされた entries を使うことができてハッピー……

そう思いましたか? 実はこれ、思わぬ落とし穴があります。
Hoge の定義を type から interface に変更してみましょう:

type Entries<T> = (keyof T extends infer U
    ? U extends keyof T
        ? [U, T[U]]
        : never
    : never)[];

function getEntries<T extends Record<string, unknown>>(obj: T): Entries<T> {
    return Object.entries(obj) as Entries<T>;
}

interface Hoge {
    a: string;
    b: number;
}

function f(x: Hoge) {
    // const entries = getEntries(x);  // コンパイル通らない!
    const entries = Object.entries(x);  // これなら通るが、 entries の型は [string, any][]
    for (const [key, value] of entries) {
        // 処理
    }
}

何故か先程の getEntries ではコンパイルが通らなくなってしまいました。
また、 Object.entries の型も [string, any][] に変わってしまいました。

何故でしょう?

実はこれ、 type ではなく interface の方が望ましい挙動なのです。

www.totaltypescript.com

このライブラリの「Object.keys / Object.entries」の欄に、その説明があります。

TypeScript is a structural typing system. One of the effects of this is that TypeScript can't always guarantee that your object types don't contain excess properties:

type Func = () => {
  id: string
}

const func: Func = () => {
  return {
    id: '123',
    // No error on an excess property!
    name: 'Hello!',
  }
}

So, the only reasonable type for Object.keys to return is Array<string>.

ざっくり翻訳すると「TypeScript は構造的型付けを採用しているので、余剰のプロパティがオブジェクトに含まれていないことを保証できない。 だから Object.keys の戻り地の型で合理的なのは Array<string> のみだ」となります。

これは Object.entries に関しても同様のことが言えます:

type Entries<T> = (keyof T extends infer U
    ? U extends keyof T
        ? [U, T[U]]
        : never
    : never)[];

function getEntries<T extends Record<string, unknown>>(obj: T): Entries<T> {
    return Object.entries(obj) as Entries<T>;
}

type Hoge = {
    a: string;
    b: number;
};

function f(x: Hoge) {
    const entries = getEntries(x);
    for (const [key, value] of entries) {
        if (key === 'a') {
            console.log(value.toUpperCase());
        } else if (key === 'b') {
            console.log(value + 1);
        } else {
            // value は never
            throw new Error('should not get here.');
        }
    }
}

// 余剰のプロパティが含まれた変数
const x = { a: 'hoge', b: 42, c: { x: 13 }, };
// これは合法な呼び出し(結果として never の部分に到達して例外が投げられる)
f(x);

つまり、先ほど紹介した記事の getEntries は、危険をはらんだコードであると言えます。
この危険を回避するためには interface を使うといいので、この記事から得られる教訓として、

「TypeScript で型を定義する際は、可能なら interface を使うこと!」

が挙げられます。
こちらも参照してください:

teratail.com



さて、とはいえ、厳密な型付けが欲しいのも確かです。 どうにかならないものでしょうか?

この場合、規定のプロパティには定められた型を与えて、余剰のプロパティに対しては unknown を推論することで何とかする、というのが、おそらく最もスマートな解決策だと思います:

type Entries<T> = /* 定義をここに書く */;

// スマートな型定義ができるなら Record である必要はない
function getEntries<T extends Object>(obj: T): Entries<T> {
    return Object.entries(obj) as Entries<T>;
}

interface Hoge {
    a: string;
    b: number;
}

function f(x: Hoge) {
    const entries = getEntries(x);
    for (const [key, value] of entries) {
        if (key === 'a') {
            console.log(value.toUpperCase());
        } else if (key === 'b') {
            console.log(value + 1);
        } else {
            // value は unknown
            console.log(value);
        }
    }
}

結論から言います。 この Entries<T> は、上手く書くことができません。
というのも、現行の TypeScript では、「 string から ある特定のリテラル型を除いた型」を定義することが出来ないからです:

type T = Exclude<string, 'a' | 'b'>;
// T は string から 'a' や 'b' を除いた型…ではない。 string になる

これを解決するために、 Negated types という機能が提案されています:

github.com

実装もあるようです:

github.com

残念ながら実装の方は merge されることなく close されてしまいましたが、あると嬉しいので、いつか実装されるといいなあと思っています。

追記

参考までに、 Negated types を用いた Entries<T> の実装を書いておきます:

type Entries<T> = (
    (keyof T extends infer U
        ? U extends keyof T
            ? [U, T[U]]
            : never
        : never)
    | [string & not keyof T, unknown]
)[];

git 管理している dotfiles をインストールするスクリプトを書いてみた

gintenlabo.hatenablog.com

の続き。


課題だったインストールスクリプトを書いてみました。

#!/usr/bin/env bash
# file: install-script.bash
set -ueo pipefail
cd "$(dirname "$0")"

CMDNAME=$(basename "$0")
print_usage() {
  cat - << EOF
usage: ${CMDNAME} (-n|-x) [options...]
    -n
        Executes dry run mode; don't actually do anything, just show what will be done.
    -x
        Executes install. This option must be specified if you want to install.
    -S <suffix>
        Specify backup suffix.
        If not given, BACKUP_SUFFIX is used; neither is given, '~' is used.
        This argument cannot be empty string.
EOF
}

MODE=
BACKUP_SUFFIX=${BACKUP_SUFFIX:-\~}

quote_each_args() {
  for i in $(seq 1 $#); do
    if [[ $i -lt $# ]]; then
      printf '%q ' "${!i}"
    else
      printf '%q' "${!i}"
    fi
  done
}
print_dry_run_message() {
  echo -e "will exec '$*'"
}
print_executing_message() {
  echo -e "executing '$*'..."
}
run() {
  if [[ "${MODE}" == 'dry-run' ]]; then
    print_dry_run_message "$(quote_each_args "$@")"
  else
    print_executing_message "$(quote_each_args "$@")"
    "$@"
    echo 'done.'
  fi
}

while getopts 'nxoS:u:m:' opt; do
  case $opt in
    n) MODE='dry-run' ;;
    x) MODE='execute' ;;
    S) BACKUP_SUFFIX="$OPTARG" ;;
    *) print_usage >&2
       exit 1 ;;
  esac
done
if [[ -z "${MODE}" || -z "${BACKUP_SUFFIX}" ]]; then
  print_usage >&2
  exit 1
fi

# init submodules
run git submodule update --init

# create symbolic links
echo
./install-script-tools/ls-linking-files.bash | while read -r file; do
  run ln -srvb -S "${BACKUP_SUFFIX}" -T "${file}" "${HOME}/.${file}"
done

# ここに残った初期化処理を書く( .gitconfig.local ファイルの作成とか vim の設定とか)
# 今回は省略
#!/usr/bin/env bash
# file: install-script-tools/ls-linking-files.bash
set -ueo pipefail

cd "$(dirname "$0")/.." # move to project root

LINK_IGNORE=${LINK_IGNORE:-.linkignore}

CMDNAME=$(basename "$0")
print_usage() {
  cat - << EOF
usage: ${CMDNAME} [options...]
    -v
        Verbose mode; print tracked files and ignored files.
    -h
        Print this message and exit.
EOF
}

VERBOSE=

while getopts 'vh' opt; do
  case $opt in
    v) VERBOSE='on' ;;
    h) print_usage
       exit ;;
    *) print_usage >&2
       exit 1 ;;
  esac
done

remove_directory_contents() {
  cat - | sed 's/\/.*//g' | sort -u
}

TRACKED_FILES=$(git ls-files -c | grep -v '^\.')
if [[ -n "${VERBOSE}" ]]; then
  echo "tracked files:" >&2
  echo -e "${TRACKED_FILES}\n" >&2
fi

IGNORED_FILES=$(git ls-files -ic --exclude-from="${LINK_IGNORE}")
if [[ -n "${VERBOSE}" ]]; then
  echo "ignored files:" >&2
  echo -e "${IGNORED_FILES}\n" >&2
fi

echo "${TRACKED_FILES}" | grep -vxFf <(echo "${IGNORED_FILES}") | remove_directory_contents
# file: .linkignore
# install-script でリンクさせたくないファイル/ディレクトリをここに置く
# 形式は .gitignore と一緒
# サブディレクトリ内のファイルは関係ないので、誤解なきよう原則として / 始まりで指定すること
/README*
/install-script*


使い方:

$ ./install-script.bash -n

と入力すると、実際に実行されるコマンドが表示される(実行はされない)。
問題ないようなら

$ ./install-script.bash -x

で実行。
この際、リンクされるファイルが既に存在していた場合にバックアップが(~/.zshrc~のような名前で)作られるので、問題なくインストール出来ているようなら削除する。
また、バックアップから復元したい場合のために、以下のようなスクリプトも書いた:

#!/usr/bin/env bash
# file: install-script-tools/restore-dotfiles-from-backup.bash
set -ueo pipefail
WORKDIR=$(pwd)
cd "$(dirname "$0")"

CMDNAME=$(basename "$0")
print_usage() {
  cat - << EOF
usage: ${CMDNAME} (-n|-x) [options...] [files...]
    -n
        Executes dry run mode; don't actually do anything, just show what will be done.
    -x
        Executes restoration. This option must be specified if you want to restore.
    -d
        Deletes given file if no backup found.
    -S <suffix>
        Specify backup suffix.
        If not given, BACKUP_SUFFIX is used; neither is given, '~' is used.
        This argument cannot be empty string.
    files
        Specify file paths to restore.
        If not given, files would be linked by install-script and ~/.gitconfig.local is restored.
EOF
}

MODE=
DELETE=
BACKUP_SUFFIX=${BACKUP_SUFFIX:-\~}

quote_each_args() {
  for i in $(seq 1 $#); do
    if [[ $i -lt $# ]]; then
      printf '%q ' "${!i}"
    else
      printf '%q' "${!i}"
    fi
  done
}
print_dry_run_message() {
  echo -e "will exec '$*'"
}
print_executing_message() {
  echo -e "executing '$*'..."
}
run() {
  if [[ "${MODE}" == 'dry-run' ]]; then
    print_dry_run_message "$(quote_each_args "$@")"
  else
    print_executing_message "$(quote_each_args "$@")"
    "$@"
    echo 'done.'
  fi
}
restore() {
  for file in "$@"; do
    local backup="${file}${BACKUP_SUFFIX}"
    if [[ -e "${backup}" ]]; then
      run rm -f "${file}"
      run mv -T "${backup}" "${file}"
    elif [[ -n "${DELETE}" ]]; then
      run rm -f "${file}"
    fi
  done
}

while getopts 'nxS:d' opt; do
  case $opt in
    n) MODE='dry-run' ;;
    x) MODE='execute' ;;
    S) BACKUP_SUFFIX="$OPTARG" ;;
    d) DELETE='on' ;;
    *) print_usage >&2
       exit 1 ;;
  esac
done
if [[ -z "${MODE}" || -z "${BACKUP_SUFFIX}" ]]; then
  print_usage >&2
  exit 1
fi

shift $((OPTIND - 1))

if [[ $# -eq 0 ]]; then
  ./ls-linking-files.bash | while read -r filename; do
    restore "${HOME}/.${filename}"
  done
else
  (cd "${WORKDIR}" && restore "$@")
fi
$ ./install-script-tools/restore-dotfiles-from-backup.bash -x ~/.zshrc

のようにして復元できる(ファイル名指定を省略した場合はリンクされるもの全部が対象になる)。


注意点として、 Mac だと ln コマンドのオプションが違うので、このままでは使えない。
後で Mac 版も書くつもり(blog で公開するかどうかは置いといて)。


実際のコードはこちら:

github.com


参考にして頂ければ幸い。

WSL Ubuntu に Oh my zsh と asdf をインストールしつつ、 .zshrc とかを git で管理するようにする

社内 Advent Calendar 向けに書いた記事ですが、社内のみに公開しておくのも勿体ないので全体公開するついでに blog にもリンク張っておきます。

gist.github.com

また、筆者が使っている dotfiles の GitHub repository も共有しておきます:
github.com

master は内容が古いので wsl-ubuntu ブランチが参考になるかと思います。

参考にして頂ければ幸い。

TypeScript で Haskell の comparing を実装してみた

https://hackage.haskell.org/package/base-4.19.0.0/docs/Data-Ord.html#v:comparing

たぶん既出(調べるの面倒で調べていない


実装:

function compareAsc<T>(x: T, y: T): number {
  return x < y ? -1 : y < x ? 1 : 0;
}
function compareDesc<T>(x: T, y: T): number {
  return -compareAsc(x, y);
}

function comparing<T, R>(
  f: (x: T) => R,
  compare: (x: R, y: R) => number = compareAsc,
): (x: T, y: T) => number {
  return (x, y) => compare(f(x), f(y));
}

function comparingWith<Obj, Key extends keyof Obj>(
  key: Key,
  compare: (x: Obj[Key], y: Obj[Key]) => number = compareAsc,
): (x: Obj, y: Obj) => number {
    return comparing((obj: Obj) => obj[key], compare);
}


使い方:

const arr = [
  {a: 1, b: 2},
  {a: -1, b: 4},
  {a: 0, b: 6},
  {a: 23, b: 8},
];

// obj.a でソート
console.log([...arr].sort(comparing(({a}) => a)));
// この場合はcomparingWithで楽に書ける
console.log([...arr].sort(comparingWith('a')));

// b で逆順
console.log([...arr].sort(comparingWith('b', compareDesc)));
// a + b でソート
console.log([...arr].sort(comparing(({ a, b }) => a + b)));


多分だけど既存のパッケージあります。 知ってたら教えてください。

TypeScript で API 呼び出し結果をキャッシュするクラスを作ってみた

実装:

class CachedAsyncStore<T, Key = string> {
  private promiseMap: Map<Key, Promise<T>> = new Map();
  private fn: (key: Key) => Promise<T>;

  constructor(fn: (key: Key) => Promise<T>) {
    this.fn = fn;
  }

  get(key: Key): Promise<T> {
    const { promiseMap } = this;
    const cachedPromise = promiseMap.get(key);
    if (cachedPromise != null) {
      return cachedPromise;
    }
    const newPromise = this.fn(key);
    promiseMap.set(key, newPromise);
    return newPromise;
  }
  deleteCache(key: Key): boolean {
    return this.promiseMap.delete(key);
  }

  async getOrRetry(key: Key): Promise<T> {
    const { promiseMap } = this;
    for (;;) {
      const cachedPromise = promiseMap.get(key)
      if (cachedPromise == null) {
        break;
      }
      try {
        const result = await cachedPromise;
        return result;
      } catch (e) {
        // retry
        if (cachedPromise === promiseMap.get(key)) {
          promiseMap.delete(key);
        }
      }
    }
    const newPromise = this.fn(key);
    promiseMap.set(key, newPromise);
    return newPromise;
  }

  clearCache(): void {
    this.promiseMap = new Map();
  }
}


使い方:

// コンストラクタに取得関数を渡す
const store = new CachedAsyncStore<{ value: string }>(async (key) => {
  console.log(`API called: key: ${key}`);
  await new Promise<void>(resolve => setTimeout(resolve, 1000));
  if (key === 'error') {
    throw new Error('error!');
  }
  return {
    value: key,
  }
});

(async () => {
  // get で API を呼び出す
  console.log(await store.get('hoge'));
  // 既に呼び出されたことがあった場合はキャッシュが使われる
  console.log(await store.get('hoge'));

  // 並列で呼び出しても API は1回のみ呼ばれる
  console.log(await Promise.all([
    store.get('fuga'),
    store.get('fuga'),
    store.get('fuga'),
  ]));

  // エラーの場合
  try {
    await store.get('error');
  } catch (e: any) {
    console.error(e.message);
  }
  // get だと API 呼び出しは再試行されずにエラーになる
  try {
    await store.get('error');
  } catch (e: any) {
    console.error(e.message);
  }

  // エラーなら再試行したい場合は getOrRetry を使う
  // 並列で呼び出した場合でも直列化されるオマケつき
  console.log(await Promise.allSettled([
    store.getOrRetry('error'),
    store.getOrRetry('error'),
    store.getOrRetry('error'),
    store.getOrRetry('error'),
  ]));
})();


動機:

// 素朴な実装の場合
class SimpleCachedAsyncStore<T, Key = string> {
  private resultMap: Map<Key, T> = new Map();
  private fn: (key: Key) => Promise<T>;

  constructor(fn: (key: Key) => Promise<T>) {
    this.fn = fn;
  }

  async get(key: Key): Promise<T> {
    const { resultMap } = this;
    const cachedValue = resultMap.get(key);
    if (cachedValue !== undefined) {
      return cachedValue;
    }
    const newValue = await this.fn(key);
    resultMap.set(key, newValue);
    return newValue;
  }
  deleteCache(key: Key): boolean {
    return this.resultMap.delete(key);
  }

  clearCache(): void {
    this.resultMap = new Map();
  }
}

const store = new SimpleCachedAsyncStore<{ value: string }>(async (key) => {
  console.log(`API called: key: ${key}`);
  await new Promise<void>(resolve => setTimeout(resolve, 1000));
  if (key === 'error') {
    throw new Error('error!');
  }
  return {
    value: key,
  }
});

(async () => {
  // get で API を呼び出す
  console.log(await store.get('hoge'));
  // 直列なら問題ない(キャッシュされた値が使われる)
  console.log(await store.get('hoge'));

  // 並列で呼び出した場合に API が複数回実行されてしまう
  console.log(await Promise.all([
    store.get('fuga'),
    store.get('fuga'),
    store.get('fuga'),
  ]));

  // エラーの場合
  try {
    await store.get('error');
  } catch (e: any) {
    console.error(e.message);
  }
  // 直列でも毎回呼び出される(エラー処理してないため)
  try {
    await store.get('error');
  } catch (e: any) {
    console.error(e.message);
  }

  // 並列の場合はエラーになるのを待たずに API が実行される
  console.log(await Promise.allSettled([
    store.get('error'),
    store.get('error'),
    store.get('error'),
    store.get('error'),
  ]));
})();


既知の問題点:

  • fn を呼び出す際に this が CachedAsyncStore のインスタンスになってしまう(本来はコンストラクタなりで thisArg を受け渡すべきなのだが、面倒なのでやっていない)
  • key 以外の引数をコールバックに渡せない( Key をオブジェクトにすると中身は比較されずインスタンス毎に別の値としてキャッシュされてしまう)
  • 1つの Promise の値に対して何回も await することになるが、規格上それで問題ないのかを調べていない(筆者の母語は C++ なので未定義動作が怖い)

他に問題点が ありましたら指摘をお願いします。

GCPのログエクスプローラで単語単位でエラーメッセージを検索したい場合は正規表現で \b を使えばいい

タイトル通り。
仕事でGCPを使う機会があり、ログを調べる際にログエクスプローラで単語単位の検索をしようと思ったが、その方法を検索しても一発では出てこなかったので、調べた結果を記す。

結論から言ってしまうと、

textPayload =~ "\bword\b"

で問題ない。 \b は単語境界にマッチする。