anti scroll

ブラウザと小説の新しい関係を模索する

AngularのExpressionChangedAfterItHasBeenCheckedErrorに関する覚え書き

ネットでExpressionChangedAfterItHasBeenCheckedErrorを検索すると、よく「子が親の状態を変更するとビューに一貫性がなくなることが警告される」という説明を目にしますが、少し混乱を招きかねない説明なんじゃないかなあと思いました。

正確には「Angularが変更検知をする際に行う探索順において、後からチェックされるビューから、先にチェックされるビューの状態を変える処理を書くと警告される」と説明すべきなのではないでしょうか。

なぜなら、HTMLには親子関係だけではなく、隣接(兄弟)関係というものがあるからです。親が子より先に探索されるのは当たり前ですが、隣接関係については自明ではありません。

例えば次のようなツリーがあったとします。

A, B
|
C

AとBは隣接(sibling)関係で、Cの親はAです。このときCの親であるAがCより先に探索されることは自明ですが、Bはどうでしょうか。Cより先でしょうか、後でしょうか。

もしAngularの変更検知が「幅優先探索」の順で行われるなら、Bが先です。しかし実際のAngularの変更検知順は「深さ優先探索」なので、検知される順に番号をふると、次のようになります。

A(1), B(3)
|
C(2)

つまりBは、Cよりも「後に」変更検知されるわけです。

だから探索順に置いて最後になるBにおいて、Cが依存するような状態を変更すると(先に確定されたCの状態を後からBが変えることになるので)ExpressionChangedAfterItHasBeenCheckedErrorというやつが出ます(ただし警告してくれるのは開発ビルドのときだけ)。

よくウェブで見る「親と子」という説明の仕方だと、幅優先か深さ優先かで受け取る人の解釈が曖昧になるので「探索順」を使った説明をしたほうが親切なのではないでしょうか。

Typescriptのアレコレ覚え書き

書いておかないと忘れちゃいそうなので、残しておきます(たまに追記するかも)。

戻り値がbooleanであると同時に、引数が特定の型であることをコンパイラに教えることができる

これが便利なのは、こういう感じのコードにおいてです。

class Foo {
  say(){
    console.log("foo");
  }
}

let isFoo = (val: any): boolean => {
  return val instanceof Foo;
}

このisFooを使って、ある値の型がFooである場合に(Fooのメソッドである)sayを呼ぶ、といった処理を書きたくて

let func = (val: any) => {
  if(isFoo(val)){
    val.say(); // val は any なのでエラー
  }
}

などとやると、コンパイル・エラーが出ます。上のコードでは(当たり前ですが)valの型がanyとみなされるからです。

しかしこれでは、せっかくFooであることを確認するisFoo関数の意味がなくなってしまいます。

しかしこの問題は、isFooを、次のように書き換えることで解決します。

let isFooEx(val: any): val is Foo {
  return val instanceof Foo;
}

戻り値の型の部分に記述されたval is Foo の働きによって、引数のvalの型は呼び出し元にFooだよ、と認識され、次のコードが通るようになります。

let func(val: any){
  if(isFooEx(val)){
    val.say(); //  val は Foo なのでOK!
  }
}

このval is Fooの部分のことを、Type Guard と呼ぶのだそうです。

詳しくは、Type Guards and Differentiating Types を参照して下さい。

型引数にもデフォルト値を設定できる

TypeScript 2.3からですが、次のような感じで型パラメータにデフォルト値を設定できるようです。

class Component<Props = any, State = any> {
  props: Props;
  state: State;
}

type StateActionPair<T, V extends Action = Action> = {
  state: T | undefined;
  action?: V;
};

二番目の例にあるV extends Action = Action の部分は、Actionの部分型であるVのデフォルト型はActionである、という意味です。

これまでこういったことを知らなくて、色々と汚いコードを書いてました…反省。

Nullチェックを無視させることができる

nullが入り得る型なんだけど、そのチェックがいらないことが明らかな場合、コンパイラnullチェックを無視させることができます。

let foo = (entity?: Entity): string {
  return entity!.name;
}

entity!という部分がそれです。

上のプログラムにおいてentityという引数は、Entity型もしくはnullもしくはundefinedになる恐れのある引数だ、と言ってるので、普通にentity.nameと書くと、コンパイルエラーになります(ただし--strictNullChecksが有効時)。

しかしこういうnullなどになり得る変数のあとに!をつけると「ここは特別にnullにならないからチェックしなくて良いよ!」という意味になります。

これも使えると時々は便利です。

参考:Non null assertion operator

undefinedならプロパティアクセスさせず全体をundefinedにする演算子

例えばこんなオブジェクトがあるとして

const obj: any = {
  foo: {
    bar: 'bar'
  }
};

次のようにアクセスするとエラーになります。

const value = obj.bar.bar //  Cannot read property 'bar' of undefined

こういう木構造の途中がundefinedかもしれないものにアクセスするときに、いちいち

const value = obj.bar? obj.bar.bar : undefined;

みたいに書くのは面倒ですよね。しかし?演算子を使うと、これを

const value = obj.bar?.bar;

というように、短く書くことができます。

仮にobj.barundefinedでも、obj.bar?で評価が止まり、全体がundefinedになるわけですね。

opam2のインストールについて

2018年9月にようやく正式リリースされた opam2

公式が提供するスクリプトでインストールできれば問題ないのですが、それだとうまくいかない環境もあり、色々とインストールが面倒だったのでメモしておきます。

ちなみに以下の内容は次のスクリプトで成功する人には何の価値もない内容ですのでご注意ください。

sh <(curl -sL https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh)

で、どうやってインストールしたかというと、自分の場合はソースからインストールしました…

ocamlbrewなる便利そうなのもあるのですが、ここ一年ぐらい更新されておらず、opam2が正式リリースされたのが先月(2018年9月)というのも考慮し、今回は見送りました。

なおopam2は4.02.3以降じゃないとビルドできませんので、先に

opam switch

とやって表示される中で、4.02.3以降のコンパイラに変更する必要があります(もちろんこれは古いままのopamで実行しても問題なし)。

自分は素直に現時点での最新バージョンを入れました。

opam switch 4.07.0

コンパイラのバージョンに問題がないなら、opam2.0.0のソースディレクトリに入ってビルドします。

./configure
make lib-ext
make
sudo make install

makeするまえにmake lib-extが入ることに注意してください。

ちなみに最後のsudo make installなのですが、ホームディレクトリの.opam/<ocamlのバージョン>/bin/以下にあるツール(jbuilderocamlfind)を使うので、普通に実行しようとすると、おそらくrootがそれらを見つけられずにコケます。

よってsudoerを編集します(他にもっといい方法が絶対にありそうなのですが、思いつかなかった…)。

visudoを起動して

Defaults env_keep += "PATH"

を追加して、

#Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin

コメントアウト。ついでに.bashprofileなどに

PATH:=$PATH:~/.opam/4.07.0/bin
export $PATH

みたいなのも追記しておきましょう(終わったらsource .bash_profileも忘れずに)。

これでローカルのjbuilderやらocamlfindやらのパス解決もできて、sudo make installが無事に通ります。

さて、インストールが終わると、新しいopamを利用するためのセットアップ処理が走るのですが、途中でbubblewrapという、サンドボックス環境を使うための仕組みを設定するかどうかを聞かれます。

セキュリティーを考えたら使ったほうが良さそうなのですが、後で調べてみたら自分の環境ではインストールできないツールだったので、インストール後に.opam/configから以下のbubblewrapに関する設定を削除しました。

# 以下の3つの設定を消す
wrap-build-commands:
  ["%{hooks}%/sandbox.sh" "build"] {os = "linux" | os = "macos"}
wrap-install-commands:
  ["%{hooks}%/sandbox.sh" "install"] {os = "linux" | os = "macos"}
wrap-remove-commands:
  ["%{hooks}%/sandbox.sh" "remove"] {os = "linux" | os = "macos"}

ちなみに理由はよくわかりませんが、先の質問に「No」と答えても、上の3つの設定は.opam/configに追加されてしまうので注意してください。

セットアップ用の質問に答え終わると、すごく長い「何かしらの処理」が始まります。

途中経過とかが全く出てこないので「え、フリーズ?」と心配してしまうかもしれませんが、気長に待ってたらちゃんと終わるので、安心して下さい。

さて、初期化の処理が終わると、これまでopam1.2系列を使っていたことで止まっていたopam upgradeが、また進むようになります。

拙作のjinja2互換テンプレートエンジンであるjingooも、opam1.2系列では1.2.18までしかインストールできませんが、opam2にすると1.2.19以降もインストールできるようになります。

公式の説明を見る限り、もうopam1.2系列はサポートしないらしいですし、既に新しいパッケージを公開する際は、opam2.0系列を強制されてしまいます。

これで先月はopam-repositoryへの新パッケージの申請が軒並みコケていて、大混乱でしたね。自分もその一人でした。

バージョン2系列はバージョン1系列のopamファイルと互換性がなく、微妙にフィールド名が変わっていたりするので注意が必要です。

自分の場合は、とりあえずopam-repositoryにopam1のままのpull requestを投げてみて、弾かれたらエラーログを見ながら直す、みたいにして、体で覚えました。

nehan version6 を公開しました

nehan version6 を公開しました。

github.com

version6はnehan.jsではなく、nehanという名前で開発することになりました。

それに伴い、リポジトリのURLなども変わっているので、ご注意ください。

変わったところ

  • Typescriptで書きました(かなり開発しやすくなりました)。
  • npm経由で使えるようになりました。
  • 組版がより正確になりました。

インストール

npm install nehan

使い方

Typescriptなら、

import * as Nehan from "nehan";

Javascriptなら

let Nehan = require("nehan");

注意点

過去バージョンのnehanもそうでしたが、古いnehan.jsとの互換性はまったくありませんので、ご注意ください!

あと古いブラウザはざっくりと切り捨ててしまったので、動かないかもしれません。

ちなみに縦書き文庫については、古いブラウザも当面サポートしなければならないので、旧バージョンのnehan.jsのまま動かしていますが、最新のNehan Readerは、このversion6を使って動かしています。もし良かったら、試してみてください。

chrome.google.com

対象年齢を設定できるようになりました

投稿画面に「対象年齢」という設定欄を新設しました。

f:id:convertical:20180126083935p:plain

ジャンルとしてはホラーだけど、内容はR18みたいなこともあるからです。

ちなみにジャンルが「官能・BL小説」の場合は、自動で「成人向け」という設定で投稿されます。

トップページにて過去のお気に入り履歴に基づいた「お勧め作品」が表示されるようになりました

トップページにて、過去のお気に入り履歴に基づいた「お勧め作品」が表示されるようになりました(ログイン時)。

新しい作品にも出会いやすくなるのではないでしょうか。

これを機に、お気に入り機能を積極的に使うきっかけになってくれたらなあ、と思います。

(参考) おすすめ作品について

switchが買えないらしいので、どのぐらい買えないのか試してみました

マリオ発売の直前に変えたら凄い、と思ってチャレンジしてみました。

まずは値動きをチェックする巡回プログラムを書きます。

#!/usr/bin/python3
# -*- coding: utf-8 -*-
from pyquery import PyQuery as pq
import os

def notify(subject, message, email):
    os.system("echo '{message}' | /bin/mailx -s '{subject}' {email}".format(message=message, subject=subject, email=email))

def parse_price(doc):
    price_tag = doc.find("#priceblock_ourprice")
    price_text = price_tag.text().replace("¥ ", "").replace(",", "");
    return int(price_text)

def check_price(url):
    doc = pq(url)
    price = parse_price(doc)
    subject = "switch price - " + str(price)
    message = url
    email = "foo@example.com"
    if price <= 33000:
        notify(subject, message, email)

if __name__ == '__main__':
    urls = [
        "https://www.amazon.co.jp/dp/B01NCXFWIZ", 
        "https://www.amazon.co.jp/dp/B01N5QLLT3"
    ]
    for url in urls:
        check_price(url)

正規価格で売られていたらメールする」というだけのプログラムです。

これを適当なレンタルサーバーで、2分おきに実行させます。

crontab -e

2/* * * * * /usr/local/bin/chkswitch.py

最後に、今まで一度も使ったことのないamazonの「1click注文」を有効にしました。

「カートに入れる」「配送方法の選択」「購入」なんてことをやってたら、その間に売り切れちゃう気がしたので…

結果

さっそく今日、5回ほどメールが届いて、即座に1clickで購入を試みてみましたが…

結果は全敗でした!

この程度の工夫では買わせてくれないみたいです。

とりあえず知見

  • 1click購入は必須だと思います(わざわざカートに入れてたら間に合わない)
  • 巡回頻度は1分ですら遅いのかも?
  • 木曜日と金曜日が狙い目と聞いてましたが、木曜日は確かにヒットしました

追記

なんか11月頃から普通に定価(3万2000円ぐらい)で買える感じになってます(自分も無事に定価で買うことができました)。

管理ページから埋め込みコードが取得できるようになりました

管理ページから埋め込みコードが取得できるようになりました。

以下の「埋め込みコード」というリンクから取得できます。

f:id:convertical:20170917162725p:plain

クリックするとコードが表示されますので、コピペして使ってください。

f:id:convertical:20170917162735p:plain