Haskellを使ってRubyのクラスメソッドを書く(その2) FFI+JSON

(2018.5.13)
[(その4)まとめ](https://qiita.com/ki073/items/2e5ee4b29aece93bf12b)を公開しました。ベンチマーク結果もありますのでご参考に。
##はじめに
[その1](https://qiita.com/ki073/items/6350747cce51e45a5e9b)の続きです。
直接FFIで数値やポインタでやり取りするとどうしてもC言語の制限が残ってしまいます。RubyやHaskellで使える大きな整数や複雑な構造の変数を気楽に扱えるJSONを経由させる方法を紹介します。
基本的には、RubyとHaskellの間でJSON形式のテキストを介してやり取りしているシンプルなものです。

ここではフィボナッチ数を1つだけ、それと配列にして求める2つを示します。

##Haskell

まずHaskellの関数を用意します。

```Haskell:src/Fib.hs
module Fib (fib) where

-- フィボナッチ数の計算
fibs = 0:1:zipWith (+) fibs (tail fibs)
fib i = fibs !! i
```

Lib.hsはそのまま使ってください。

```Haskell:src/Lib.hs
{-# LANGUAGE OverloadedStrings #-}
module Lib where

import Foreign.C.String
import Data.ByteString.Char8 (unpack, packCString, pack)
import qualified Data.ByteString.Lazy.Char8 as BL8
import Data.Aeson

-- CStringをAesonが解釈できるData.ByteString.Lazy.Char8に変換し、funcに渡しその結果を、CStringに戻す
convertCStringViaJSON :: (BL8.ByteString -> BL8.ByteString) -> CString -> IO CString
convertCStringViaJSON func cstr = do
    b8s <- packCString cstr
    newCString . unpack . BL8.toStrict $ func $ BL8.fromStrict b8s

-- JSON(Data.ByteString.Lazy.Char8)をdecodeし、funcで変換後にencodeする
convertJSONBy :: (ToJSON a, FromJSON t) => 
    (t -> a) -> BL8.ByteString -> BL8.ByteString
convertJSONBy  func json_str = encode $ func d
    where d = case eitherDecode json_str of
              Right a -> a
              Left  s -> errorWithoutStackTrace s -- 型が違うとエラーに

-- debug用
toBL8 :: String -> BL8.ByteString
toBL8 string = BL8.fromStrict $ pack string
```
上のconvertCStringViaJSONはCStringとData.ByteString.Lazy.Char8との変換、convertJSONByはJSONテキストとデータとの変換をしています。

```Haskell:app/Main.hs
module Main where

import Foreign.C.String
import Foreign.Marshal.Alloc (free)
import GHC.Ptr
import Lib
import Fib

main = undefined

-- 整数値のJSONを受け取って、fibを計算してJSONで返す
fib_h cstr = convertCStringViaJSON (convertJSONBy (fib :: Int -> Integer)) cstr
foreign export ccall fib_h :: CString -> IO CString

-- 配列形式のJSONを受け取って、fibを計算してJSONで返す
fibs_h cstr = convertCStringViaJSON (convertJSONBy (map fib :: [Int] -> [Integer])) cstr
foreign export ccall fibs_h :: CString -> IO CString

foreign export ccall "free_ptr" free :: Ptr a -> IO ()
```

(fib :: Int -> Integer)と(map fib :: [Int] -> [Integer]))の違いだけでデータ型の違いを吸収しています。この部分だけを書き換えることで色々な関数やデータ構造に対応できます。

使われている関数(ここではfib)の型注釈は、型推論する上で重要ですので忘れないようにしてください。
decode(eitherDecode)やencodeは型推論が正常にできないと簡単にエラーになってしまいます。debug用にtoBL8を追加していますので、エラーが出るようでした、色々と試してみてください。

```
$ stack ghci
> :l Lib
*Lib> decode $ toBL8 "[1,2,3]" :: Maybe [Int]
Just [1,2,3]
*Lib>  decode $ toBL8 "[1,2,\"a\"]" :: Maybe (Int, Double, String)
Just (1,2.0,"a")
```

最後の例にありますようにタプルへの変換もできます。(Ruby)Array<->(Haskell)List or Tuple間の変換がなされます。

コンパイル等はその1を参考に
今回はData.ByteStringやData.Aesonを使っていますので、package.yamlにdependenciesを追加しておいてください。

```package.yaml
その1からの変更点
(省略)
library:
  source-dirs: src
  dependencies:
  - aeson
  - bytestring
(省略)
```

##テスト用プログラム

```Ruby:test.rb
require "ffi"
require "json"

module Test
	extend FFI::Library
	ffi_lib "./.stack-work/install/x86_64-osx/lts-11.5/8.2.2/bin/testFFI.so"
	attach_function :c_hs_init, :hs_init, [:pointer, :pointer], :void
	attach_function :hs_exit, [], :void
	attach_function :hs_free, :free_ptr, [:pointer], :void	
	def self.hs_init() c_hs_init(nil, nil) end
	
	attach_function :fib_h, [:string], :pointer
	attach_function :fibs_h, [:string], :pointer
	
	def self.ffi_via_JSON(y)
	    ptr= yield y
		read_str=ptr.read_string
		self.hs_free(ptr)
		JSON.parse(read_str)
	end
	
	def self.fib(v)
        self.ffi_via_JSON(v){|a| self.fib_h JSON.generate(a)}
	end

	def self.fibs(v)
	    self.ffi_via_JSON(v){|a| self.fibs_h JSON.generate(a)}
	end
end
Test.hs_init
p Test.fib(20)
p Test.fibs([*100..110])
Test.hs_exit
```