KRAZY感情TEXTYLE

"くれいじー かんじよう てきしたいる" と読みます

goroutineで自作ランタイム上にsetTimeoutを再現する(実装の手順とcallback-hellもついてくるよ!!)

こんばんは、id:Kroutonです。RustのFutureについて調べていたはずが*1いつの間にかcallbackでsetTimeoutを再現するコードをGoで書いていたのでその実装手順について簡単に書きたいと思います。実用性はほぼないです。

注意書き

筆者がプログラミングを始めた頃にはもうES6(というかBluebirdを使ったPromise)が広まりかけた頃なので、体験したことありませんし間違ってるかもしれません。また、本エントリを読んだことによるPTSDやフラッシュバックなどの症例については一切の責任を負いません。

要件

var rt = NewRuntime()

func main() {
	rt.run(func() {
		println("これからsetTimeoutをやっていきます")
		setTimeout(1500, func() {
			println(3)
		})
		setTimeout(2000, func() {
			println(4)
		})
		setTimeout(2, func() {
			println(1)
			setTimeout(3000, func() {
				println("こんな風にネストもできるよ、はいおしまい!")
			})
		})
		setTimeout(1000, func() {
			println(2)
		})
		println("1->2->3->4の順番で出るはず")
	})
}

標準出力

これからsetTimeoutをやっていきます
1->2->3->4の順番で出るはず
1
2
3
4
こんな風にネストもできるよ!はいおしまい!

Playground

play.golang.org

初手

package main

import (
	"time"
)

// TODO: そのうちフィールドを用意する
type Runtime struct{}

func NewRuntime() *Runtime {
	return &Runtime{}
}

func (rt *Runtime) run(program func()) {
	program()
}

var runtime = NewRuntime()

// とりあえず今はsleepして実行するだけ
func setTimeout(ms int, callback func()) {
	time.Sleep(time.Duration(ms) * time.Millisecond)
	callback()
}

func main() {
	runtime.run(func() {
		println("開始")
		setTimeout(100, func() {
			println(2)
		})
		// 待つ時間が少ないこっちが先に実行されたい
		setTimeout(20, func() {
			println(1)
		})
                 setTimeout(1000, func() {
		        println("終わり")
                 })
	})
}

このコードは

開始
2
1
終わり

の順に出力されるので改良が必要ですね(といっても並行処理も何もしてないの当たり前ですが)

setTimeoutでgoroutineを呼んでみる

setTimeoutの実装を次のように変えてみましょう

func setTimeout(ms int, callback func()) {
	go func() {
		time.Sleep(time.Duration(ms) * time.Millisecond)
		callback()
	}()
}

そうすると、実行結果が

開始

になりますね。goroutine自体は作れているのですが、その中でprintlnを呼ぶ前にmainの処理が終わってしまうのでこのようになってしまいます。
なので、次はmain関数が終わらないようにしてみましょう。

メインスレッドを無限ループさせる

Runtime#runを次のようにしましょう

func (rt *Runtime) run(program func()) {
	program()
	for {
	}
}

こうすると結果は

開始
まずはこっち
OK
終わり
...(ただし実行は終わらない)

と正しい順番になりつつも無限ループが終わらないといった感じになりますね。ここまで来たら、channelを用意してgoroutine内で値を上手く渡せば無限ループをbreakできそうな感じがしませんか?というわけでそろそろRuntimeのフィールドを追加しましょう。

Runtimeにフィールドを追加してsetTimeoutを実装しきる

type Runtime struct {
	nextID    int
	callbacks map[int]func()
	recv      chan int
}

func NewRuntime() *Runtime {
	recv := make(chan int)
	return &Runtime{recv: recv, callbacks: map[int]func(){}}
}

func (rt *Runtime) register(callback func()) int {
	registeredID := rt.nextID
	rt.callbacks[registeredID] = callback
	rt.nextID++
	return registeredID
}

func setTimeout(ms int, callback func()) {
	callbackID := runtime.register(callback)
	go func() {
		time.Sleep(time.Duration(ms) * time.Millisecond)
		runtime.recv <-callbackID
	}()
}

setTimeoutが呼ばれると、Runtimeのcallbacksにユニークなid(この場合はint)をkey、渡されたcallbackをvalueとしてmapに追加します。そして、goroutine内で指定された時間だけ待った後、 用意したchannel(この場合はruntime.recv)にmapに追加したkeyを送信します。

完成

func (rt *Runtime) run(program func()) {
	program()
	for {
		callbackID := <-rt.recv
		callback := rt.callbacks[callbackID]
		delete(rt.callbacks, callbackID)
		callback()
		if len(rt.callbacks) == 0 {
			break
		}
	}
}

最後はRuntime#runを弄って完成です。 setTimeoutで送信されたkeyを受信して、そのcallbackを引いてきてmapから削除し、実行します。登録されたcallbackがすべて空になったら無限ループが終わる。といった感じです。

あとがき

本当はRustを使ったmspcで書きたかったけどGoの方需要がある気がしたので書きなおしました、まあチャンネルがある言語なら大体同じだと思うので好きな言語で書いてみてください、全然わからんとか間違えてるとかご意見あったら
https://twitter.com/Krout0nまで

追記 2020/07/06

package main
<200b>
import (
	"sync"
	"time"
)
<200b>
func run(program func()) {
	program()
	wg.Wait()
	println("Done")
}
<200b>
var wg = &sync.WaitGroup{}
<200b>
func setTimeout(ms int, callback func()) {
	wg.Add(1)
	go func() {
		time.Sleep(time.Duration(ms) * time.Millisecond)
		callback()
		wg.Done()
	}()
}
<200b>
func main() {
	run(func() {
		println("開始")
		setTimeout(100, func() {
			println("OK")
		})
		// 待つ秒数が少ないこっちが先に実行されたい
		setTimeout(20, func() {
			println("まずはこっち")
		})
		setTimeout(1000, func() {
			println("終わり")
		})
	})
}

これだけでいいらしい、goroutine周りの道具がしっかりあってGoいい感じですね

*1: Introduction - Futures Explained in 200 Lines of Rust このRuntimeのコードもほぼここに乗ってる実装と同じです。 cssamsonさんに圧倒的感謝

thread_localメモ

自分用覚書、thread_local!で定義したstatic変数はスレッド毎に LocalKeyという構造体でwrapされる。各スレッド変数はそれぞれ外側に不変である、可変参照をしたいならRefCellを被せたりする。

use std::cell::RefCell;

thread_local! {
    static X: RefCell<Vec<usize>> = RefCell::new(vec![]);
}

fn main() {
    X.with(|v| {
        println!("initial {:?}", v.borrow());
    });

    let j = std::thread::spawn(move || {
        X.with(|v| {
            v.borrow_mut().push(1000);
            println!("other thread {:?}", v.borrow());
        });
    });
    j.join().unwrap();
    X.with(|v| {
        println!("after other thread {:?}", v.borrow());
        v.borrow_mut().push(1024);
    });

    main_other_fn();
}

fn main_other_fn() {
    X.with(|v| {
        println!("main_other_fn {:?}", v.borrow());
    });
}

initial
other thread [1000]
after other thread

main_other_fn [1024]

SEKIROトロコンした

こんにちは、id:Krouton です。初任給でゲーミングPCを買ったんですけど本当に光るんですね、感動しました。

それはそれとしてこのPCでSEKIROをトロコンしたのでエントリ書きます。Discordで友達にGo Liveで配信しながらやってました。途中から社会人のくせに頻度高すぎて誰も来なくなったのは内緒。

f:id:Krouton:20200525105250p:plain
全体の7.1%って結構少ない気がする?
f:id:Krouton:20200525102727j:plain
Steam版です
f:id:Krouton:20200525102730j:plain
IGTは47時間半とかなのでそこそこ早い方?

初見はWikiなど禁止、ただし動画勢の友人達ががヒカルの碁のサイのように助言をしてきたりはありましたが。 2週目からはWiki解禁して最低限のボス+気分で中ボスだったので効率重視でやってました。

TL;DR

未トロコン勢

弦ちゃん倒せたらトロコンできるからそこまで頑張って

その他

いいゲームだから動画エアプ勢もその他も買って(はぁと

SEKIRO: SHADOWS DIE TWICE - PS4

SEKIRO: SHADOWS DIE TWICE - PS4

  • 発売日: 2019/03/22
  • メディア: Video Game

以下雑に語る

思い出が残ってるところだけ、ネタバレありなので注意されたい。

続きを読む

自作言語の構文を考えるのがめんどくさいそこのアナタ、Rustのマクロで楽してもいいわよ?

最近 とっくんのYouTubeチャンネル - YouTube さんの再度ハマり言語野が支配されてます、id:Krouton です。

事の発端

つまり「言語を作りたいけど具象構文考えるのめんどくせ〜〜!!!!」「パーサジェネレータ使うにしてもそれすら書き下すのもめんどくせ〜〜!!!!!」って時ありますよね?(あってくれ)
そんなアナタのためにS式でASTを表現するのをオススメします。Rustが書きたかったのでそうしましたが、他のマクロがサポートされてる言語なら大体できそう?

TL;DR と まとめ

  • (1 + 2) + (3 + 4) を表現するのに
BinOP(
    Box::new(BinOP(Box::new(Number(1)), Add, Box::new(Number(2)))),
    Add,
    Box::new(BinOP(Box::new(Number(3)), Add, Box::new(Number(4)))),
)

から

ast!((+ (+ 1 2) (+ 3 4)))

という風にS式で表現できるようになる。Box::newを手書きするのから解放されるようになる。

  • 簡単にコンパイラのミドルエンド*1から手をつけられるようになる。
  • 言語処理系が書きたくなる。

本編

type AST = Expression;

#[derive(Debug, PartialEq)]
pub enum Expression {
    Number(usize),
    Ident(String),
}

use Expression::*;

impl From<usize> for Expression {
    fn from(n: usize) -> Self {
        Number(n)
    }
}

impl<'a> From<&'a str> for Expression {
    fn from(s: &'a str) -> Expression {
        Ident(s.to_string())
    }
}

macro_rules! ast {
    ($i:ident) => {
        Expression::from(stringify!($i))
    };
    ($e:expr) => {
        Expression::from($e)
    };
}

fn main() {
    dbg!(ast!(2)); // Number(2)
    dbg!(ast!(id)); // Ident("id")
}

ここはFromを使ってるだけで特に難しいところはありませんね。ASTとマクロを拡張して関数適用(Apply)を考えてみましょう。

#[derive(Debug, PartialEq)]
pub enum Expression {
    // 略
    Apply {
        fn_lit: Box<Expression>,
        args: Vec<Expression>,
    },
}

macro_rules! ast {
    (($fn:tt $( $arg:tt )*)) => {
        Apply {
            fn_lit: Box::new(ast!($fn)),
            args: vec![$( ast!($arg), )*]
        }
    };
    // 略
    ($e:expr) => {
        Expression::from($e)
    };
    // exprより後にしないと1 や 2が全てIdentになるので最後に置く
    ($t:tt) => {
        Expression::from(stringify!($t))
    }
}

ast!((print 1 2)); // Apply { fn_lit: Ident("print"), args: [Number(1), Number(2)] }
ast!((+ 1 2)); // Apply { fn_lit: Ident("+"), args: [Number(1), Number(2)] }

ミソとしては四則演算を関数適用として、stringify!で+などの演算子をIdentとして解釈する部分です。これのおかげで、四則演算用のマクロのルールを定義せずに表現できるようになりました。
あとは好きな風にASTを拡張して好きなS式で表現しましょう!

拡張したgist gist.github.com

Special Thanks

*1: コンパイラのミドルエンドって滅多に使わない言葉らしいってWikipedia先生が言ってた コンパイラ - Wikipedia

*2: 質問者は私です

Goの[]byte -> stringへのcastってshrinkするんですか?

珍しく技術系の投稿をします(ただし疑問、忘備録

tooEnoughBuf := make([]byte, tooEnoughSize)
Write1(tooEnoughBuf, src)
string(tooEnoughBuf) == string(src) // false

をしたい時のbufの長さをshrinkする方法を考えています。

なぜこの等価判定がfalseになるかというと

len(tooEnoughBuf) == len(src)

がfalseになるからですね。

func shrinkString(s string) string {
	var eos byte
	buf := make([]byte, 0)
	for _, b := range []byte(s) {
		if b == eos {
			break
		}
		buf = append(buf, b)
	}
	return string(buf)
}

のような実装を与えてあげればいいのだろうか・・・?わからない・・・

株式会社はてなに入社しました

id:Krouton です。株式会社はてなに新卒入社しました。
本日からWebアプリケーションエンジニアです。
株式会社はてなに入社しました - hitode909の日記


所属してる会社は隠そうと思いましたが、GitHubのOrganizationを見れば分かってしまうし隠すのもなんか嫌なのでエントリにすることにしました。試用期間中にリストラされないことを祈っています。
されたら誰か誘ってください。とりあえずがんばります。

www.amazon.jp

五カ年計画(後付)した電気通信大学を卒業しました

こんばんは。3/31で無事卒業できたので電通大卒業エントリを書こうと思います。今は無き情報理工学部の先端工学基礎課程(以下、K課程)に所属していました。

動機

親「国公立に入れ」
僕「家から近いから横国がいいな〜、勉強無理だ。電通大ってとこあるじゃん、前期受けよ!!」

センター試験

僕「緊張しまくって数学半分も解けなかった・・・、もう無理だ、ごちうさでも見てよう」
ごちうさ「線路へぽっぴんジャンプ♪ 恥決定♪」
僕「ア・・・、ア・・・・・」

前期試験後

僕「無理だな、浪人!!」
一緒に電通大受けたフォロワー「俺も無理だった〜、夜間主が2次募集してるんだけど受ける?」
僕「受ける!」

と言った流れで受験し、無事合格しました。フォロワーは落ちました。*1
K課程の2次募集は 3/29-30(午前) に募集、3/30(午後)に合格発表というなかなかアツいスケジュールでした。一人暮らしの人どうやって物件決めたんだ・・・。

1年前期(2015)

Twitterで気が合いそうなオタクを探しながら授業を受ける、バイトをしないといけなかったので授業中に探したりするも1ヶ月ぐらい連続で落ち続ける。*2
プログラミングに興味があったので学内のサークルに入るも、経験がなく何も分からないし、ノートPC持ってなかったので手を動かせずに消える。
Vim vs Emacsみたいなことをしていてたくさん会話してて面白そうだけど一切わからんしここは俺のためのところじゃないなぁと幽霊になる。
結局ファミマでバイトをしてました。初の落単を経験する。

1年後期

幽霊してたサークルの同学年のオタクが技術系バイトをしてることを知る。当人に「最低賃金で立ちっぱのバイトつらそう」と言われ、悔しくなったのでバイト代でMBPを買った。

苦しんで覚えるC言語

苦しんで覚えるC言語

  • 作者:MMGames
  • 発売日: 2011/06/24
  • メディア: 単行本
たのしいRuby 第6版 (Informatics&IDEA)

たのしいRuby 第6版 (Informatics&IDEA)

このへんの本 *3を読んで筋力をつけた。特に実装したいものもなかったが、コードを書きたかったので、基礎科学実験の計算結果を求めるためだったり、LaTeXの表のコードを吐かせるコードを書いたりしてた。実務未経験でバイトを探すのは本当に難しかったが、やる気を買っていただいた人たちと大学名に感謝。Pythonでクローラ書いたりしてた。

2年前期(2016)

SIerっぽいとこがバイト先になった。d3.jsでグラフ書いたり、出たばっかのSwift3を使ってiOSアプリのプロトタイプ版みたいなのを一人で作ったりしてた。JSもSwiftも何もわからなかった。

2年後期

学生ベンチャー企業と掛け持ちした。JSとAWSでサーバサイドをやってた。心理的安全性がありえん低く辛かったのに加え、年明けに高1から付き合ってた彼女に振られて心が壊れる。電車の中で涙が出てきたり、途中下車して号泣したりしてた日もあった。

3年前期(2017)

心が壊れてました。土曜1限起きれなくて落単、これが留年のきっかけになる。あとインターンシップが必修であったので、夏休みに2週間フルタイムで行ってた。そこではCircleCIとシェルをにらめっこしてた。

3年後期

初めて心療内科に行く。行くまでは自分の精神も管理できないどうしようもなくダメな人間だと思っていたけど、見方が変わった。SICPを少し読んだ。インターン先がバイト先になった。ScalaとTypeScriptを雰囲気で書いてた。それまで静的型付けがない言語を触っていたが、型があるといいな〜とか思い始める。この辺から言語に対する興味が強くなった。

4年前期(2018)

セキュキャンのCコンパイラゼミに通る。自分より圧倒的にすごい人達に圧倒される。謙虚になった。セルフコンパイルはできてないし未だにずっと「やろうやろう」考えている。本当に。
knium.hatenadiary.com

K課程は4年前期まで必修が誰でもあるカリキュラムになっているのだが、3年前期の落単科目が突然時間割変更して4年の必修科目に衝突する。留年確定。

4年後期

2科目ぐらいしかなかったのでほぼ引きこもりみたいになってた。わからん。記憶がない。科目はすべて必修だが、出席を取らなかったのでほとんど出た記憶がない。テスト前に教えてくれた友人達に圧倒的感謝。

5年前期

タスクを複数同時に持つと死ぬことを学ぶ。複数持ってたタスクは全部投げ出した。院試を受けたけど全然ダメだった。単位取りきった。就活もした。

5年後期

休学、インターンとバイト、卒業

振り返ってみて

授業の記憶ない・・・。通学に往復3時間かかるとそれだけで起きてる時間の1/5は持ってかれるのでできるだけ近くに住んだほうがいい、でも自分みたいな人間は一人暮らししたら破滅が見えるので難しいね。
4年前期に留年が確定してなかったらセキュキャンに行くことはなかったと思う。*4
GPAが低い自分が言うのもアレですが、情報やプログラミング、CS系は努力した分だけ報われるのがよく分かる分野だと思う。たぶん真の意味で技術は好きじゃないのかもしれないけど、生きる分にはそれでもいいかなってなってきた。何回も挫折したし、やめたくなったけど何とか辞めずに続けたし、これからも何とか続けて生きていたいなぁと思います。

*1: 翌年無事に合格したっぽい

*2: その時までにコンビニ2年半ぐらい経験してたのに・・・

*3: 結局制御構造まではわかったけどポインタはわからんかったぐらい、たのしいRubyは正直覚えてない

*4: 院試と時期被ってたし