Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

TypeScriptのExcludeはなぜT extends K ? never : Tで実装できるのか?

直感に反しているExclude型についてconditional typeの話をしつつ解説します。

Exclude型とは?

Union型から特定の要素を取り除く型です。ある型から特定のプロパティを取り除きたいときに使えます。

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

type PersonWithoutAge = Pick<Person, Exclude<keyof Person, "age">>;

Exclude型の実装と解説

Exclude型は簡潔に実装できます。

type Exclude<T, K> = T extends K ? never : T;

わけがわからないと思うので1つずつ解説します。

extendsキーワードと三項演算子

extendsは「右辺は左辺のサブクラスであるか」を判定します。特にここではプロパティ名同士の比較になるので、単に等しいかどうかを判定しています。

そして、T extends Kが真であったならneverを、偽であったならばTを返します。

動作をみて分かるとおり、これは三項演算子、もっといえばif文のような役割を果たしています。そのため、名前もconditional typeとなっています。

さて、ここまで読んでいて「おや?」と混乱してきませんか?僕もとても困惑しました。なぜならExclude<keyof Person, "age">は書き下すとkeyof Person extends "age" ? never : keyof Personとなるわけで、意味がわからないからです。ご安心ください。これであっています(この書き下しは間違えています)。このあときちんと説明します。

conditional typeは分配法則が適用される

conditional typeはT extends KのTがUnion型の場合、以下のように分配が起きます。

T extends K ? never : T
-> T = (U1 | U2 | U3)とする
-> (U1 extends K ? never : U1) | (U2 extends K ? never : U2) | (U3 extends K ? never : U3)

これを見ればExclude<keyof Person, "age">でなぜうまく行くかはほとんど明らかですね。

参考

TypeScript 2.8 の Conditional Types について - Qiita

JavaScriptでPriorityQueue

優先度付きキューを実装する必要に駆られたので書きました。

実装

TypeScript

class PriorityQueue<T> {
    private container: T[] = [];
    private size = 0;
    private comp: (a: T, b: T) => boolean;
    constructor(comp = (a: T, b: T) => a < b) {
        this.comp = comp;
    }
    push(val: T) {
        let index = this.size;
        this.container[this.size] = val;
        ++this.size;

        for (let parent = 0|(index-1)/2;
             parent >= 0 && this.comp(this.container[parent], this.container[index]);
             index = parent, parent = 0|(index-1)/2) {
                 const tmp = this.container[index];
                 this.container[index] = this.container[parent];
                 this.container[parent] = tmp;
             }
    }
    pop() {
        this.container[0] = this.container[this.size-1];
        this.container.pop();
        --this.size;

        for (let index = 0, l = 1, r = 2;
             l < this.size && (this.comp(this.container[index], this.container[l]) || this.comp(this.container[index], this.container[r]));
             l = index * 2 + 1, r = index * 2 + 2) {
                 let target = l;

                 if (this.comp(this.container[index], this.container[r])) target = r;
                 if (this.comp(this.container[index], this.container[l]) && this.comp(this.container[index], this.container[r]))
                     if (this.comp(this.container[r], this.container[l])) target = l;
                 else target = r;

                 const tmp = this.container[index];
                 this.container[index] = this.container[target];
                 this.container[target] = tmp;
                 index = target;
             }
    }
    top() { return this.container[0]; }
    empty() { return this.size == 0; }
}

JavaScript

class PriorityQueue {
    constructor(comp = (a, b) => a < b) {
        this.size = 0;
        this.container = [];
        this.comp = comp;
    }
    push(val) {
        let index = this.size;
        this.container[this.size] = val;
        ++this.size;

        for (let parent = 0|(index-1)/2;
            parent >= 0 && this.comp(this.container[parent], this.container[index]);
            index = parent, parent = 0|(index-1)/2) {
            const tmp = this.container[index];
            this.container[index] = this.container[parent];
            this.container[parent] = tmp;
        }
    }
    pop() {
        this.container[0] = this.container[this.size-1];
        this.container.pop();
        --this.size;

        for (let index = 0, l = 1, r = 2;
            l < this.size && (this.comp(this.container[index], this.container[l]) || this.comp(this.container[index], this.container[r]));
            l = index * 2 + 1, r = index * 2 + 2) {
            let target = l;

            if (this.comp(this.container[index], this.container[r])) target = r;
            if (this.comp(this.container[index], this.container[l]) && this.comp(this.container[index], this.container[r]))
            if (this.comp(this.container[r], this.container[l])) target = l;
            else target = r;

            const tmp = this.container[index];
            this.container[index] = this.container[target];
            this.container[target] = tmp;
            index = target;
        }
    }
    top() { return this.container[0]; }
    empty() { return this.size == 0; }
}

使い方

new PriorityQueue()でインスタンスを生成できます。デフォルトは降順で要素が出てきます。

const q = new PriorityQueue<number>();
for (const e of [4, 1, 6, 2, 5, 3, 9, 10, 8, 7]) {
    q.push(e);
}
while (!q.empty()) {
    console.log(q.top());
    q.pop();
}

比較関数を渡すことで優先順を変えることができます。

const q = new PriorityQueue<number>((a:number, b:number) => a>b); // 昇順
for (const e of [4, 1, 6, 2, 5, 3, 9, 10, 8, 7]) {
    q.push(e);
}
while (!q.empty()) {
    console.log(q.top());
    q.pop();
}

サブディレクトリでNext.jsのアプリをデプロイしたい!!

結構こういう要求はあるとおもいます。

解決する課題

  • /staticへのリンクが途切れる
  • <Link>で遷移するとbasepathが変わる

つまり、サブディレクトリ以下でNext.jsを使ったSPAをしたい!!という人向けの記事です。

/staticへのリンクをサブディレクトリ以下になるようにする

たとえばexample.com/next-appでデプロイしたいとき、example.com/next-app/_next/static以下にJSファイルなどは置かれます。しかし、なにも設定しないままだとNext.jsではexample.com/_next/staticへのリンクを張るため404となってしまいます。

これを解決するには、next.config.jsでassetPrefixを設定する必要があります。

const isProd = process.env.NODE_ENV == 'production';

const url = isProd ? 'https://example.com/next-app' : '';
module.exports = {
    assetPrefix: url,
}

これでローカルでも本番環境でも/staticへアクセスできるようになります。

<Link>で遷移するとbasepathが変わる

上の方法で/staticへアクセスできるようになりました。しかし、実はルーティングをしている場合これだけでは対応が不十分です。

たとえば以下のような<Nav>を考えます。

import * as React from 'react';
import Link from 'next/link';

const Nav = () => (
    <nav>
        <ul>
            <li><Link href="/"><a>Top</a></Link></li>
            <li><Link href="/about"><a>About</a></Link></li>
            <li><Link href="/content"><a>Content</a></Link></li>
        </ul>
    </nav>
);

このNav要素はローカル環境ではなんの問題もなく動作します。しかし、本番環境ではうまく動作してくれません。たとえば本番環境でAboutをクリックすると、example.com/aboutへとURLが変わります。もうおわかりですね。この状態でリロードするとページが読み込まれません。

この問題の解消方法はとても簡単です。as属性をつけてあげるだけです。

<Link href="/about" as="/next-app/about"><a>About</a></Link>

しかし、今度はローカルでうまく動作しません。困りましたね。

設定で切り替える

参考: Deploy your NextJS Application on a different base path (i.e. not root)

どうやらNext.jsにはnext.config.jsから値を取ってくる機能があるようです。

import getConfig from 'next/config';

const {publicRuntimeConfig} = getConfig();

こうするとnext.config.jsで設定したpublicRuntimeConfigの値を取ってこられるようになります。あとはこれを設定して、Linkのasでこれを使った設定をしてやれば完成です。詳しいコードはこちらを参考にしてください。

参考

Codeforcesで精進するときに便利なツールの紹介

github.com

今回はこちらのツールを紹介します(なぜか日本語記事でCodeforces向けのツール紹介がなかったので)

ツール概要

このツールは以下のような便利機能を提供しています。

  • テストケースのダウンロード・ディレクトリセットアップ
  • テンプレートファイルからコードの生成
  • 提出
  • コンテストへの参加・カウントダウン表示

online-judge-toolsやatcoder-cliをご存知なら、それのCodeforces版のようなものです。

インストール

コンパイル済みのバイナリをダウンロードするか、ソースコードからビルドします。

ダウンロードはこちらから Releases · xalanq/cf-tool · GitHub

ソースコードからビルドする場合は次のようになります。Goがインストールされている必要があります。

$ go get github.com/xalanq/cf-tool
$ cd $GOPATH/src/github.com/xalanq/cf-tool
$ go build -ldflags "-s -w" cf.go

ログイン

インストールしたら、まずCodeforcesへログインします。cf configコマンドを実行し、なにを設定するか問われるので0と入力してログイン設定へ進みます。その後必要な情報を入力してログインします。

使用例

たとえば Problem - 279B - Codeforces こちらの問題を例に見てみます。

  1. テストケースをダウンロード: cf parse 279
  2. ディレクトリへ移動: cd ./cf/contest/279/b
  3. ファイルを編集: code b.cpp
  4. テスト: cf test
  5. 提出: cf submit

atcoder-cliと似ていることが分かるかと思います。

cf submitすると、コードのジャッジ状況がターミナル上に表示されて更新されていきます。提出結果を確認するためだけにブラウザを開かずに済むので楽です。

ちょっとだけ便利にする

軽いラッパーを噛ませて、cf tでテスト、cf sで提出できるようにします。

以下はzsh用のラッパーになります。

cf() {
    if [ "s" = "$1" ]; then
        command cf submit ${@:2}
    elif [ "t" = "$1" ]; then
        command cf test ${@:2}
    else
        command cf ${@:1}
    fi
}

denite.nvimからfzf.vimに移行してみた

tl;dr

  • 操作性が良い
  • :Agコマンドが強すぎる
  • めっちゃカッコイイ!!

fzf.vimとは?

fzf.vimとはfzfを使ってファイルオープンやバッファ切り替えを行うツールです。denite.nvimと役割はかなり近いです。

fzf.vimの使い方

requirement

fzf本体を別途インストールする必要があります。公式ではホームディレクトリにインストールしていますが、$XDG_CACHE_HOME以下にインストールするとホームディレクトリが汚れなくて快適です。

$ git clone https://github.com/junegunn/fzf "$XDG_CACHE_HOME/fzf"
$ $XDG_CACHE_HOME/fzf/install --xdg --no-key-bindings --completion --no-update-rc

オプションはお好みで。

Gitを使う方法以外に、vimのプラグインとして、zshのプラグインとして管理する方法もあります(しかし結局、上の方法が一番問題が起こりづらいです)。

fzf.vimのインストール

fzf.vimをインストールします。以下ではdein.nvimを例に挙げています。その他の環境の場合はfzf.vimをご覧ください。

[[plugins]]
repo = 'junegunn/fzf.vim'
on_cmd = [
    'Files',
    'ProjectFiles',
    'Buffers',
    'BLines',
    'History',
    'Tags',
    'BTags',
    'GFiles',
    'Ag',
]
hook_add = '''
nnoremap <silent> ,a :<C-u>Ag<CR>
nnoremap <silent> ,f :<C-u>ProjectFiles<CR>
nnoremap <silent> ,b :<C-u>Buffers<CR>
nnoremap <silent> ,m :<C-u>History<CR>
set rtp+=$XDG_CACHE_HOME/fzf "$XDG_CACHE_HOMEの下にインストールした場合
" set rtp+=~/.fzf "~/.fzfにインストールした場合
'''

基本設定はこれでOKです。以下はかっこよくしたり、プロジェクトディレクトリで開くためのコードです。

hook_source = '''
function! s:find_git_root()
  " プロジェクトルートで開く
  return system('git rev-parse --show-toplevel 2> /dev/null')[:-2]
endfunction

command! ProjectFiles execute 'Files' s:find_git_root()
" command! -bang -nargs=? -complete=dir Files
"     \ call fzf#vim#files(<q-args>, fzf#vim#with_preview(), <bang>0)
command! -bang -nargs=? -complete=dir Files
    \ call fzf#vim#files(<q-args>, {'options': ['--layout=reverse', '--info=inline', '--preview', 'head -20 {}']}, <bang>0)



" Terminal buffer options for fzf
autocmd! FileType fzf
autocmd  FileType fzf set noshowmode noruler nonu

" 見た目をいい感じにする
" 参考: https://github.com/junegunn/dotfiles/blob/master/vimrc
"   https://github.com/junegunn/dotfiles/blob/master/vimrc
if has('nvim')
  function! s:create_float(hl, opts)
    let buf = nvim_create_buf(v:false, v:true)
    let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
    let win = nvim_open_win(buf, v:true, opts)
    call setwinvar(win, '&winhighlight', 'NormalFloat:'.a:hl)
    call setwinvar(win, '&colorcolumn', '')
    return buf
  endfunction

  function! FloatingFZF()
    " Size and position
    let width = float2nr(&columns * 0.9)
    let height = float2nr(&lines * 0.6)
    let row = float2nr((&lines - height) / 2)
    let col = float2nr((&columns - width) / 2)

    " Border
    let top = '╭' . repeat('─', width - 2) . '╮'
    let mid = '│' . repeat(' ', width - 2) . '│'
    let bot = '╰' . repeat('─', width - 2) . '╯'
    let border = [top] + repeat([mid], height - 2) + [bot]

    " Draw frame
    let s:frame = s:create_float('Comment', {'row': row, 'col': col, 'width': width, 'height': height})
    call nvim_buf_set_lines(s:frame, 0, -1, v:true, border)

    " Draw viewport
    call s:create_float('Normal', {'row': row + 1, 'col': col + 2, 'width': width - 4, 'height': height - 2})
    autocmd BufWipeout <buffer> execute 'bwipeout' s:frame
  endfunction

  let g:fzf_layout = { 'window': 'call FloatingFZF()' }
endif
'''

使い方

:Filesまたは,fと入力するとカレントディレクトリのファイルが列挙された状態になります。エンターを押すと選択したファイルを新しいバッファーで開きます。カーソルはC-nC-pで候補を移動でき、<TAB>を押すと複数選択できます。

:Buffersは開いているバッファーの一覧を表示します。こちらは複数選択はできません。

agコマンドがインストールされている場合、:Agコマンドが利用できます。こちらはag '^(?=.)'の検索結果(カレントディレクトリ以下のすべてのファイルの内容)に対してfzfでフィルタリングするというものになっています。非常に動作が軽くて快適です。エンターを押すと該当ラインにカーソルが当たった状態でバッファーを開きます。

denite.nvimとfzf.vimの違い

PROS CONS
denite.nvim ・ ソースが豊富
・ registerを扱える
・ フィルタリングのレスポンスがfzfより悪い
fzf ・ 操作性が良い
:Agが便利
・ 動作が軽い
・ denite.nvimのregisterにあたるものがデフォルトではない

こんな感じです。フィルタリングがサクサクできるのはfzf.vimの大きなメリットだと思います。