heredocsの紹介

  • 9
    Like
  • 0
    Comment

[背景] コード生成はやっぱり(++)じゃないよね

サーバサイドをHaskellで書いてクライアントサイドをC#で書けないかと少しservantを試したことがあって,その時にservantのコードからC#のコード生成をするライブラリを作成した.
このような時にやっぱり(++)とかで文字列連結するようなのは可読性も保守性も最悪だ.

コード生成については以前metadataというschema.orgのリソースをHaskellの型で提供するライブラリを生成するプログラムを書いた時にもやっぱり書いて案の定保守しきれなくなった.

ヒアドキュメント

Perlとか古典的なLLだとコード生成はヒアドキュメントかな.多分.
やっぱり生成したいコードがそれらしく見える方が圧倒的に分かりやすい.
調べたらheredocというライブラリがあったけどメンテが2013でストップしていてしかも記述力の面で不満が.
shakespeare的なのが浮かんだがシンプルにStringやTextを生成するのが意外とみあたらず作りました.

heredocs

というわけでheredocsの紹介.

shakespeareにインスパイアされているので次の様な構文が使える.
基本は[heredoc|Hello|]みたいにTemplateHaskellを使っている.

$let

\$letでバインド.

sample0.hs
[heredoc|
Question
  $let x = 42
    ${show x} + 2 = ${show $ x + 2}.
    ${show x} * 2 = ${show $ x * 2}.
    ${show x} ^ 2 = ${show $ x ^ 2}.
|]

インデントすれば

sample1.hs
[heredoc|
Question
  $let x = 42
    $let y = "Katsutoshi"
      ${y}(${show $ x+3}).
|]

$maybe-$nothing

つづいて\$maybe-\$nothingね.

sample2.hs

let mu = Nothing
in [heredoc|
$maybe u <- mu
  Hello ${u} san
$nothing
  Bye
|]

\$nothingは無くても良い.

sample3.hs
let ma = Nothing :: Maybe Int
in [heredoc|
$maybe a<-ma
  ${show a}
|]

$if-$else

sample4.hs
[heredoc|
$if True
  OK
$else
  NG
|]

これも\$elseは無くてもOK.

$case-$of

\$case-\$ofもサポートしているからこれでも書ける.

sample5.hs
let mp = Just (Person "Katsutoshi" 45 Male)
in [heredoc|
$maybe Person name age sex <- mp
  $case sex
    $of Male
      ${name}(${show age}) - Otoko
    $of Female
      ${name}(${show age}) - Onna
    $of _
      ${name}(${show age}) - ?
|]

$forall

繰り返すときは\$forall

sample6.hs
let ps = [Person' "katsutoshi" 45 Male, Person' "keiko" 44 Female]
in [heredoc|
$forall (i, p) <- zip [1,2] ps
  ${show i}
    Name : ${name p}
    Age  : ${show $ age p}
    Sex  : ${show $ sex p}
|]

パターンマッチ系

アズパターン使える.

sample7.hs
[heredoc|
$maybe p@(Person _ age _) <- Just (Person "katsutoshi" 45 Male)
  ${show p}
|]

リストとかタプル使ったバインドもOK.

sample8.hs
[heredoc|
$let xss@(x@(_,_):xs) = [(1,2),(3,4),(5,6)]
  ${show $ fst x} and ${show xs} in ${show xss}
|]

IsStringに対応

sample9.hs
let name = "Katsutoshi" :: Text
in [heredoc|Hello, ${name} (Text) san!|]

とか

let name = "Katsutoshi" :: ByteString
in [heredoc|Hello, ${name} (ByteString) san!|]

でわざわざT.unpackとかBS.unpackしなくても良い.

その他,詳しくはheredocのテストコードを参照してみてください.
pull request歓迎.

heredocFile

このテのライブラリとしては当然APIに

Heredoc.hs
heredocFile :: FilePath -> Q Exp

も用意しているのでもちろんテンプレートをファイルにしておいてHaskellコードに埋め込まなくてもOK.

使用例

そもそもservant-csharpを書くのに使いたかったという話でした.
というわけで最後にこんな感じになりましたという実例を.

CS/JsonDotNet.hs
apiCsFrom :: (Monad m, HasForeign CSharp Text api,
              GenerateList Text (Foreign Text api)) =>
             GenerateCsConfig -> Proxy api -> SwagT m String
apiCsFrom conf api = do
  uas <- prims
  return [heredoc|/* generated by servant-csharp */
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
#region type alias
$forall (n, t) <- uas
  using ${T.unpack n} = ${showCSharpOriginalType t};
#endregion
namespace ${namespace conf}
{
    class ServantClient : HttpClient
    {
        public ServantClient()
        {
            this.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        }
    }
    public class API
    {
        #region fields
        private string server;
        #endregion
        #region properties
        #endregion
        #region Constructor
        public API(string _server)
        {
            this.server = _server;
        }
        #endregion
        #region APIs
        $forall ep <- getEndpoints api
          $if retType ep /= "void"
            public async Task<${retType ep}> ${methodName ep}Async(${paramDecl ep})
          $else
            public async Task ${methodName ep}Async(${paramDecl ep})
          {
              var client = new ServantClient();
              var queryparams = new List<string> {
                  $forall (_, qp) <- queryparams ep
                    _${qp}.HasValue ? $"_${qp}={_${qp}.Value}" : null,
              }.Where(e => !string.IsNullOrEmpty(e));
              var qp= queryparams.Count() > 0 ? $"?{string.Join("&", queryparams)}" : "";
              $if requestBodyExists ep
                #if DEBUG
                var jsonObj = JsonConvert.SerializeObject(_obj, Formatting.Indented);
                #else
                var jsonObj = JsonConvert.SerializeObject(_obj);
                #endif
              $if requestBodyExists ep
                var res = await client.${methodType ep}Async($"{server}${uri ep}{qp}", new StringContent(jsonObj, Encoding.UTF8, "application/json"));
              $else
                var res = await client.${methodType ep}Async($"{server}${uri ep}{qp}");
              Debug.WriteLine($">>> {res.RequestMessage}");
              $if requestBodyExists ep
                Debug.WriteLine($"-----");
                Debug.WriteLine(jsonObj);
                Debug.WriteLine($"-----");
              Debug.WriteLine($"<<< {(int)res.StatusCode} {res.ReasonPhrase}");
              var content = await res.Content.ReadAsStringAsync();
              Debug.WriteLine($"<<< {content}");
              $if retType ep /= "void"
                return JsonConvert.DeserializeObject<${retType ep}>(content);
              $else
                JsonConvert.DeserializeObject(content);
         }
          public ${retType ep} ${methodName ep}(${paramDecl ep})
          {
              $if retType ep /= "void"
                Task<${retType ep}> t = ${methodName ep}Async(${paramArg ep});
                return t.GetAwaiter().GetResult();
              $else
                Task t = ${methodName ep}Async(${paramArg ep});
                t.GetAwaiter().GetResult();
          }
        #endregion
    }
}
|]

あとservant-csharpは現在休止中.
swagger経由すればいいだけだということがその後に分かったからです.
次これ関係でやるとしたらswaggerから自分にとって使いやすいC#プロジェクトを生成するとかやりたくなったら別プロジェクトでやるかなーってところ.

This post is the No.22 article of Haskell Advent Calendar 2016