haskell-jp / beginners #18 at 2021-09-30 10:06:10 +0900

こんにちは、Template Haskellを書いている最中に
Illegal variable name: Mail
というエラーが出てきて困っています。

エラーとしては
mailscript/app/Main.hs:18:1: error:
    Illegal variable name: 'Mail'
    When splicing a TH declaration:
      main_0 = do {bodyTpl_1 <- eitherParseFile "./template/body.txt";
             titleTpl_2 <- eitherParseFile "./template/title.txt";
             Data.Traversable.mapM (\property_3 -> GHC.Base.pure (Language.Haskell.TH.Syntax.BindS (Language.Haskell.TH.Syntax.VarP (Language.Haskell.TH.Syntax.mkName GHC.Base.$ ("input" GHC.Base.++ property_3))) (Language.Haskell.TH.Syntax.VarE System.IO.getLine))) ps;
             let {env_4 = fromPairs [(Data.Text.Lazy.unpack GHC.Base.$ Lib.key (Lib.Schema (Data.Text.Lazy.pack "mail") [Data.Text.Lazy.pack "todayWorkSchedule",
                                                                                                                         Data.Text.Lazy.pack "todayOutcome",
                                                                                                                         Data.Text.Lazy.pack "tomorrowWorkSchedule",
                                                                                                                         Data.Text.Lazy.pack "thoughts"])) Data.Aeson.Types.ToJSON..= (Data.Aeson.Types.ToJSON.toJSON GHC.Base.$ Data.Foldable.foldl (GHC.Base.$) Main.Mail ps)];
                  body_5 = Data.Either.either GHC.Err.error toStrict GHC.Base.$ (bodyTpl_1 GHC.Base.>>= (`eitherRender` env_4));
                  title_6 = Data.Either.either GHC.Err.error toStrict GHC.Base.$ (titleTpl_2 GHC.Base.>>= (`eitherRender` env_4))};
             uprint body_5;
             uprint title_6}
   |
18 | do
   | ^^...

現在、会社の日報のためにメール送信スクリプトを作成しようと考えています。jsonでデータの構造を書いておき、そのインスタンスをEDEでメールの定型文に読み込む方針で す。

対象となるコードはこちらになります。Template Haskell自体まだ概念に馴染めておらず、なかなか苦戦しております…
なにかおすすめの資料等あれば教えていただけるとありがたいです:writing_hand:

data Properties = Properties [Text] deriving Show
instance FromJSON Properties where
    parseJSON (Array v) = Properties <$> parseJSON (Array v)
    parseJSON _ = mzero

data Schema = Schema
    { key :: Text
    , properties :: [Text]
    } deriving (Lift, Show)
instance FromJSON Schema where
    parseJSON (Object v) =
        if size v /= 1 then
            fail "expected a pair."
        else
            do hs <- parseJSON (Object v)
               let ls = toList hs
               return . (\(key, Properties properties) -> Schema key properties) $ head ls


toTitle :: Text -> String
toTitle =
    let
        toTitleString (c:cs) =
            (toUpper c):cs
    in
    toTitleString . unpack


schemaToData :: Schema -> Q [Dec]
schemaToData schema =
  do
    name <- ( newName . toTitle $ key schema )
    pure
      [ DataD [] name [] Nothing
        [ RecC
            ( mkName . toTitle $ key schema )
            ( Prelude.map
                (\property -> ( mkName $ unpack property, Bang Language.Haskell.TH.NoSourceUnpackedness Language.Haskell.TH.NoSourceStrictness, ConT ''Text ))
                ( properties schema )
            )
        ]
        [ DerivClause Nothing [ConT ''Show, ConT ''Generic]
        ]
      , InstanceD Nothing [] (AppT (ConT ''ToJSON) (ConT name)) []
      , InstanceD Nothing [] (AppT (ConT ''FromJSON) (ConT name)) []
      ]
(\property -> ( mkName $ unpack property, Bang 

の行で、 mkName に渡している unpack property の結果の文字列が大文字始まりになっていることが原因と思われます。
この property は外から入ってきたデータのようなので、これ自体に大文字始まりの文字列が入ってしまっている、もっと根本的な原因は分からないです。
あと、Template Haskellで構文木を組み立てるときは、極力 [d| |][e| |] などのquote構文を使いましょう。これを使うと、直接Haskellのコードを書いて構文木を組み立てることができて、劇的にコード量が減りますし、Template Haskellの仕様変更にも強くなります。例えば拙作ですがtypesafe-precureというパッケージでは、今回のケースのように外部からのデータで型を作成してそのインスタンス定義までしている箇所がたくさんあります。
具体的には https://github.com/igrep/typesafe-precure/blob/411e39147b068ffa9e98a73175e244f07c39aa86/src/ACME/PreCure/Types/TH.hs#L139-L205 のあたり。

中に何を $( ) で埋め込めるかが分かりづらくて、あらゆるケースで使えるわけじゃないのでご注意ください。多分そのあたりでハマると思うのでそのときは改めて教えてください。

さらにもう一つ。上記の方法がうまく使えず、 DataDRecC といった値コンストラクターを直接使って構文木を組み立てる場合においても、よりよい方法があります。
https://hackage.haskell.org/package/template-haskell-2.17.0.0/docs/Language-Haskell-TH-Lib.html こちらのモジュールで定義されている、`dataD` などの関数を使いましょう。こちらは直接 Q Monadの値を返してくれるので pure を使う必要性が減るだけでなく、値コンストラクターではなく関数なので互換性を維持しやすい(Template Haskellの仕様変更に少し強い)というメリットもあります。
Template HaskellはGHCのメジャーバージョンが変わる毎にちまちまと仕様変更を繰り返しているので、これらの方策でなるべく変更に強いコードにしておきましょう。
Template Haskellの [e| ... |] などについてもっと詳しく解説してる記事があれば誰補足してほしいです... :pray:
ご返信ありがとうございます!
data コンストラクタだから勝手に大文字始まりだと思いこんでいました……Nameあたりまたドキュメント読み直して調べてみます!

実は<@U4LGTMTMK> さんのタイプセーフプリキュアの記事をhaskell-jpで見て、template haskellに興味を持ったんです。
ソースコードを呼んでみたいと思います。ありがとうございます!

stageやlift等コンパイラに怒られてしまって、ちょっとまだ難しいと思いdataD等避けていました。
これからは関数で記法を統一していこうと思います。ありがとうございます。:pray:
@igrep
すごく小さなスクリプトですが、とうとう完成できました!ありがとうございます!
stage restrictionで長い間手こずりましたが……

ここで質問があります。
1. 日本語を表示する際unicode-showを使っているが、“が追加されるのを回避する方法はあるか?(showの挙動なので回避できない?)
2. SMTPでメール送信をしたいと考えているが、その際のメールアカウントのパスワードはどのように保存するべきか?

1番に関しては、後々TUIにしようと考えているので大きな問題ではないですが、もし何か解決法があるならお伺いしたいです。
馬鹿な勘違いでした:persevere:

2番に関しては秘密鍵をコンパイル時に生成してコードに埋め込み、暗号化したパスワードをファイルに保存しようかと思っています。
これってセキュリティ的に問題があるんですかね?(使用するのは個人のパソコンなので問題ないのか?とも思っていますが……)
取り急ぎ 1. だけ。状況がよくわからないんですが、普通に putStr などで直接出力するのではダメなんでしょうか?
お恥ずかしい……putStrでもエスケープされると思い込んでました…!
repl脳になってました:persevere:
たしかに考えてみたら、printで制御文字とかを表示するための便利仕様という位置づけでしたね…ありがとうございます!
2. については、秘密鍵を実行ファイルに書き込んでしまうと結局外のファイルに書き込んでおくのとあまり差がないのではないかと思います。もちろん、リバースエンジニアリングして複合する難易度は多かれ少なかれ上がるでしょうけども。
できるだけ安全にしたいのなら、可能なら、起動時に hSetEcho stdin False した上で 標準入力などから入力を隠した状態で読んでおいてオンメモリーでしか保持しない、という手があります。
ただ単純にこの方法だけだと、メモリーダンプから読み取れてしまう恐れなどがあるので(まぁ、それも個人のパソコンに入られてしまってる時点でどうやねんという話ではあるんですが)、
https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/c607.html
で紹介されているような保護機構が使えるのではないかと思います。
ただ、これをHaskellから手軽にやるライブラリーがあるかは知りません... FFIだけでできるのかな...
迅速な返信ありがとうございます!
ディスク上に書き出させないようにして、強制終了された時などに盗まれないようにするものなんですね。

私はまだ初心者で前提知識が足りなさすぎると感じるので、今回はこの方法は見送ることにします…。ありがとうございます。
UNIXやwindowsなどのOSの知識も重要ですね。すべてやったほうが良い知識ではあるからどこから手を付けたらいいのやら:sweat_smile:

今回はいっそのこと暗号化しなくても良いかもしれないです。入られたらもろとも終わりや精神で!高くを自分に求めすぎると挫折してしまうので:relieved:
とりあえず hSetEcho stdin Falseした上でインメモリーに持っていくのは、うっかり画面を覗かれてもれる、みたいなリスクを抑えられるのでおすすめですよ!まあ、起動時に標準入力が使えないなどの問題があるのかも知れませんが...