Hatena::ブログ(Diary)

かきにっき

2011-12-25

[][][] Scheme golf tips

あなごる

Haskell golf の教科書を読んで「俺もこういうの書きたい!」と思って書いてみた.(思ってから書くまでに1年くらい経ってるけど)

アマチュアゴルファーなので至らぬ所もありありだけど,今後のScheme界とあなごる界の盛り上がりを願ってこの文章を公開する.

(教科書を参考にさせていただきました.)


危険なので golf 以外では真似しないで下さい!


あなごるの基本

  • 余計な空白は除去
  • 改行はLF
  • エラーで止めてよい
  • 出力の末尾に余計な空白,改行があっても構わない

Scheme(Gauche) golfの基本

  • とにかくまずは適当に書いてプログラム運算していく
  • 他の言語よりgcdが安いので素数整数系の解き方が少し違うことがある
  • use はしない方が短いことも多い

よく使うのは srfi-1,特に iota.

srfi-1 で簡単に書けても書き下した方が短いこともあるので,とにかくいろいろ書いてみることが重要

S式の短縮記法を活用する

(quote x)
'x
(list x y)
`(,x,y)
(dotimes(i 10)(print'hoge)
(dotimes,10(print'hoge))
(dotimes(i 10)(print(+ i 1))
(dotimes,10(print(+ . ,1)))
(dotimes,10(print(+ .,1))) ; 0.9.2以降
(let((f proc))(f x)(f y))
(let('proc)'x'y)

Gauche0.9.1ではunquoteを束縛してもquasiquote中のunquoteは動くので安心して使える

(let(,print)`(,(list 1 2 3))) ; => ((1 2 3))

(修正 12/26 8:29)

安心して使えないバグが判明.

(let (,1 (x 2)) `(,x)) ; => 2

そもそも上記のようにunquoteが使えてしまうのがバグなので,最新の開発版ではquasiquote自体が健全なように修正された.

(let (,1) `(,(+ 1 2))) ; => (,(+ 1 2))

共有構造(SRFI-38)

(print(read-line)(read-line))
(print #0=(read-line)#0#)

generic function や短縮名を活用する

^ ~ .$Gauche 0.9.1 から.

^lambda の短縮名.

(lambda(x y)...)
(rec(_ x y)...)
(^(x y)...)

場合によってはさらに

(^'y ...)
(lambda(x)...)
(rec,x ...)
(^x ...)

let1 よりも短くなる.

(let1 x(foo)(bar x))
((^x(bar x))(foo))

~ref* の短縮名で, ref*ref のネストをまとめたもの. reflist-refvector-ref のgeneric版.

(list-ref l i)
(ref l i)
(~ l i)
(ref(ref l i)j)
(~ l i j)

.$compose の短縮名.

(map f(map g l))
(map(.$ f g)l)

その他

(string->number s)
(x->number s)

文字列はシンボルで代用できることがある

(print"hoge")
(print'hoge)
'("foo""bar""baz")

もシンボルでよければ

'(foo bar baz)

string-interpolation

文字列の構成はこれか format ですることが多い.

(x->string x)
#`",x"

共有構造はread単位でしか共有されないため,

#`",#0=x,#0#"

とはできない.

正規表現と object apply

文字列処理手続きは string- がついて高いので,なるべく正規表現+object applyで済ませる.

文字列の分解は大体これでやる.

(substring x 1(-(string-size x)1))
((#/.(.*)./x)1)
(string-copy x 1)
((#/.(.*)/x)1)

set!の返り値

(現在の)Gaucheでは(多分),R5RS形式の set! は代入される値を返す.SRFI-17形式の set! は setter が返す値を返す. vector-set! とかは #<undef> を返すようなので都合よく縮んだりしない.最初から使える一文字の識別子には ^~ がある. ^x がとても安いので使う機会は減ったが,空白の関係で縮むことがある.

anarchy golf - NABEATSU of the world

順次実行

順次実行する時は大体 print とかなので , beginifand で代用できる.

(begin(print x)x)
(if(print x)x)
(begin(print x)(set! y z)x)
(and(print x)(set! y z)x)   ; z が #f でないとき

分岐

他言語と違って普通に if or and が安い.cond や case もたまに使う.

文字は(Gaucheでは) eq? で比較できるが,数で扱えば = で比較できる.

文字列化して正規表現でマッチするかどうか調べると縮む場合がある.

null? よりは eq?()Gaucheでは () はquoteしなくても動く.

(null? l)
(eq?()l)

入力

read で済むなら read .文字単位の処理は read-charread-byte

たまに read-block も使うが,これは incomplete string を返すので使いにくい(正規表現refが使えない)ので気をつける.

出力

基本は print .リストを出力する場合,(apply print ...)の形も有効.

文字列の構成は #`"..."format が多い.

再帰/ループ

(for-each(^x ...)l)
(map(^x ...)l)

for-each は勿論使わない.

(while(read-line)=> x ...)
(port-map(^x ...)read-line)

(while(read-line)...) はエラー止めが必要なので,できない場合は port-map を使う.

束縛する必要がなければ

(while(print(~(read-line)0)))

等.

入力に対するループはdo dotimes rec while until port-map port-fold等,選択肢が多い.

目安は (修正 1/17 17:58)

  • ただの反復は whileport-map
  • 入力に対する反復で状態変数を一つ持つ場合は rec
  • 入力に対する反復で状態変数を一つ持ちつつエラー止めできない場合は port-fold
  • 綺麗に当てはまるものがなければ do

do

(do('car,cdr`print(x 1)(i 0(+ i 1)))(#f)...)

といろいろできるので強い.

整数に対する繰り返しは dotimesiotasrfi-42 を使う場合と,他のループと inc! dec! を合わせて使う場合がある.

入力を一度リストにしたい時は

(port-map(^x x)read-line)

となるが,+* が一引数の時に引数をチェックせずそのまま返すので

(port-map + read-line)

と書ける.

2011-12-21

[]Scalaでidを書こうとした

最初,無名関数で書こうとしたら,どうすれば多相的になってくれるのかわからなくて,ぐぐってたら名前を付ければいいということだった.

ところが,

scala> def id[T](x: T) = x
id: [T](x: T)T

scala> id _
res0: Nothing => Nothing = <function1>

scala> (id _)(42)
<console>:9: error: type mismatch;
 found   : Int(42)
 required: Nothing
              (id _)(42)
                     ^

( ゚Д゚)


(追記 12/23 11:59)

scala> ((identity _):Int => Int)(42)
res8: Int = 42

scala> (((identity _):(Int => Int) => (Int => Int))(identity _))(42)
res9: Int = 42

型を書けば動く.手前に書いた場合は推論してくれる.

メソッドは多相的になれるけど関数は多相的になれないのかな.

2011-11-26

[]京都大学11月祭ぷよぷよ通大会

f:id:gengar:20111126182848j:image:right

ふぁいやー(京都大学ぷよぷよサークル) 動画倉庫に参加.

有名人が少なかったので予選突破した.明日東大で大会があるらしくてその影響なんだとか.

予選16勝(勝率7割程度?),準決勝リーグ1勝6敗(2-13)で敗退.

予選15勝の人は延長戦があったようだ.

今年は去年ほど速攻されなかった気がするが,組み合わせ運の問題かもしれない.

2011-10-06

[][]Gauche素数ストリーム

エラトステネスの篩

(define (divisor? x y)
  (eqv? 0 (remainder x y)))

(define-constant +primes+
  (stream-cons 2 (stream-filter prime? (stream-iota -1 3 2))))

;; (define (prime? n)
;;   (not (stream-any (cut divisor? n <>) (stream-take-while (cute >= (sqrt n) <>) +primes+))))

(define (prime? n)
  (let1 limit (sqrt n)
    (let loop ((srm +primes+))
      (let1 x (stream-car srm)
        (or (< limit x)
            (if (divisor? n x)
                #f
                (loop (stream-cdr srm))))))))

;(time (stream-ref |+primes+| 10000))
; real   0.868
; user   0.840
; sys    0.020

; => 104743

stream-take-while を使った版は 5.132s かかった.

Gaucheでエラトステネスの篩で素数ストリーム(世間一般のやつより速い) - Gemmaの日記 のと比較してみたら,向こうのは 7.792s かかったのでこちらの方が大分速い.

でもこれを Haskell に移植して Haskell 版で比較すると 0.179s と 0.123s で向こうの方が速かった.

ところが,-O つけてコンパイルするとどちらも 0.104s くらいで同じになった…

takeWhile を使うと 0.695s と遅くなっていたのが -O 付きでは 0.078s と一番速くなった.


HaskellのtakeWhile展開版

primes = 2 : filter isPrime [3,5..]
{-
isPrime n = not $ any (isDivisor n) $ takeWhile (limit >=) primes
    where
      limit = floor $ sqrt $ fromIntegral n
-}
isPrime n = loop primes
    where loop (x:xs) | limit < x = True
                      | isDivisor n x = False
                      | otherwise = loop xs
              where limit = floor $ sqrt $ fromIntegral n

isDivisor n m = rem n m == 0

main = print $ primes !! 10000

2011-09-05

[]「F#をまねる」をまねる

F# の |> 演算子がかっこよいので、Haskell で作ってみた。

infixl 0 |>
(|>) :: a -> (a -> b) -> b
a |> f = f a

以下のようにシェルのパイプのような感じで使う。

foo :: String
foo = [1..100]
   |> map (*2) 
   |> filter (\x -> x `mod` 6 == 0) 
   |> sum
   |> show
http://d.hatena.ne.jp/kazu-yamamoto/20110218/1297992233

Schemeマクロを活用して真似てみる.

(define-syntax >>
  (syntax-rules ()
    ((_ value)
     value)
    ((_ value (proc args ...) expr ...)
     (>> ((cut proc args ... <...>) value) expr ...))))

(define-syntax >>*
  (syntax-rules ()
    ((_ value)
     value)
    ((_ value (proc args ...) expr ...)
     (>>* (call-with-values (lambda () value)
            (cut proc args ... <...>)) expr ...))))

名前は適当,>>*は多値対応版だけどGaucheでは遅くなってしまった.気にする所じゃなさそうだけど.

使い方は

(>> (iota 100) (filter odd?) (map (pa$ * 2)) (fold + 0))
; => 5000
(>> (iota 100) (filter odd?) (map (pa$ * 2)) (take <> 10))
; => (2 6 10 14 18 22 26 30 34 38)
(>>* (values 7 2) (quotient&remainder) (+))
; => 4

(追記6:46)

@dico_leque さんにGaucheではcall-with-valuesよりreceiveの方が少し速いと教わったので書いてみた.速くなった.Gaucheではreceiveの方がプリミティブでcall-with-valueはただの手続きとのこと.

(define-syntax >>+
  (syntax-rules ()
    ((_ value)
     value)
    ((_ value (proc args ...) expr ...)
     (receive vars value
       (>>+ (apply (cut proc args ... <...>) vars) expr ...)))))

名前はますます適当である.

Gauche 0.9.2 で計測.コード適当だけど受け渡し部分を計りたいんだからこれでいいよね?

;(time (dotimes (_ 100000) (>> 1 (+ 2) (+ 3) (+ 4) (+ 5) (+ 6) (+ 7))))
; real   0.231
; user   0.220
; sys    0.000
;(time (dotimes (_ 100000) (>>* 1 (+ 2) (+ 3) (+ 4) (+ 5) (+ 6) (+ 7))))
; real   0.395
; user   0.380
; sys    0.010
;(time (dotimes (_ 100000) (>>+ 1 (+ 2) (+ 3) (+ 4) (+ 5) (+ 6) (+ 7))))
; real   0.341
; user   0.330
; sys    0.000

(追記19:53)

(define-syntax $>
  (syntax-rules (=>)
    ((_ value)
     value)
    ((_ value => proc expr ...)
     ($> (proc value) expr ...))
    ((_ value (proc args ...) expr ...)
     ($> ((cut proc args ... <...>) value) expr ...))))
($> 3 => (^x (+ x x)) (- <> 1))
; => 5