haskell-jp / questions #102 at 2022-12-23 23:48:59 +0900

こんばんは。2 次元の動的計画法をシュッっと書きたいのですが、 ST モナドなどでハマっています。
お力添えいただけないでしょうか (スレッドに続く) 。
[1] 実現したいこと
以下の疑似コードに相当する関数を作りたいです:
def run_dp(user_function, bounds):
    array := default_2d_array(bounds)
    for (y, x) in bounds:
        array[y, x] = user_function(array, (y, x))
    return array

[2] 実現するために自分が試した内容とその結果
IOArray を使うと、似た関数をコンパイルできました ():
tabulateIO :: forall i e. (Ix i) => (IOArray i e -> i -> IO e) -> (i, i) -> e -> IO (IOArray i e)
tabulateIO f bounds_ e0 = do
  tbl <- newArray bounds_ e0 :: IO (IOArray i e)
  forM_ (range bounds_) $ \i -> do
    e <- f tbl i
    writeArray tbl i e
  return tbl

問題点:
1. 大量にメモリを消費しました (unboxed array ではないため?) 。また
IOUArray を使おうとするとエラーになりました (`newArray` の箇所で Could not deduce (MArray IOUArray e IO))

2. 利用側のコードで unsafeFreeze するのが手間でした。また
IOArrayrunSTArray + STArray に置き換えることができませんでした

[3] 原因だと思われそうな箇所
1. 型引数 e の制約が緩い (`Int`, Char など unboxed な型に限定する必要がある?)
2. 引数 f の利用が ST モナドの制限に (なぜか) 引っかかる
指摘点や『こう書けば良いのでは』などあればお願いします :pray:
IOUArray への変更は下記のようにできました
#!/usr/bin/env stack
{- stack script --resolver lts-16.11
--package array --package bytestring --package containers
--package vector --package vector-algorithms --package primitive --package transformers
-}

{- ORMOLU_DISABLE -}
{-# LANGUAGE BangPatterns, BlockArguments, LambdaCase, MultiWayIf, PatternGuards, TupleSections #-}
{-# LANGUAGE NumDecimals, NumericUnderscores #-}
{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, ScopedTypeVariables, TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{- ORMOLU_ENABLE -}

-- {{{ Imports

module Main (main) where

import Control.Monad
import Data.Char
import Data.List

{- ORMOLU_DISABLE -}

-- array
import Data.Array.IArray
import 
import Data.Array.MArray
import 
import Data.Array.Unsafe

import qualified Data.Array as A
import qualified Data.Array.Unboxed as AU

-- bytestring: 
import qualified Data.ByteString.Char8 as BS

-- vector: 
import qualified Data.Vector.Unboxed as VU

{- ORMOLU_ENABLE -}

getLineIntList :: IO [Int]
getLineIntList = unfoldr (BS.readInt . BS.dropWhile isSpace) <$> BS.getLine

getLineIntVec :: IO (VU.Vector Int)
getLineIntVec = VU.unfoldr (BS.readInt . BS.dropWhile isSpace) <$> BS.getLine

tabulateIO :: forall i e. (Ix i, MArray IOUArray e IO) => (IOUArray i e -> i -> IO e) -> (i, i) -> e -> IO (IOUArray i e)
tabulateIO f bounds_ e0 = do
  -- FIXME: "Could not deduce (MArray IOUArray e IO)" with `IOUArray`
  tbl <- newArray bounds_ e0 :: IO (IOUArray i e)
  forM_ (range bounds_) $ \i -> do
    e <- f tbl i
    writeArray tbl i e
  return tbl

main :: IO ()
main = do
  [nItems, wLimit] <- getLineIntList
  wvs <- VU.replicateM nItems $ (\[a, b] -> (a, b)) <$> getLineIntList

  let dp = tabulateIO f rng (0 :: Int)
      rng = ((0, 0), (nItems, wLimit))
      f :: IOUArray (Int, Int) Int -> (Int, Int) -> IO Int
      f _ (0, _) = return 0
      f tbl (i, w) = do
        let wv = wvs VU.! (i - 1)
        v1 <- readArray tbl (i - 1, w)
        v2 <- if w - fst wv >= 0 then (snd wv +) <$> readArray tbl (i - 1, w - fst wv) else return 0
        return $ max v1 v2

  dp' <- dp
  dp'' <- unsafeFreeze dp' :: IO (AU.UArray (Int, Int) Int)

  print $ maximum [dp'' ! (nItems, w) | w <- [0..wLimit]]
ポイントは tabulateIOMArray IOUArray e IO 制約ですね
元々の tabulateIO の型だと e に関する制約がないので指定してやる必要があります
まさにこれですね
1. 型引数 e の制約が緩い (`Int`, Char など unboxed な型に限定する必要がある?)
ST 版はここまで書いて runSTUArrayeMArray (STUArray s) e (ST s) を満たすよって教えられなかったのでちょっと引数工夫しないといけなさそう(`RankNTypes` 有効)
tabulateIO :: forall i e. (Ix i) => (forall s. MArray (STUArray s) e (ST s) => STUArray s i e -> i -> ST s e) -> (i, i) -> e -> UArray i e
tabulateIO f bounds_ e0 =
  runSTUArray uarray
  where
    uarray :: forall s. MArray (STUArray s) e (ST s) => ST s (STUArray s i e)
    uarray = do
      -- FIXME: "Could not deduce (MArray IOUArray e IO)" with `IOUArray`
      tbl <- newArray bounds_ e0 :: ST s (STUArray s i e)
      forM_ (range bounds_) $ \i -> do
        e <- f tbl i
        writeArray tbl i e
      return tbl
型引数にせず具体的な Int にする手もありますね
MArray IOUArray e IO
なるほど! `e` の部分だけ型パラメータにして、他は具体的な型で埋めたら良かったですね。今、 Num Int みたいな無意味な制約を加えてキャッキャしてました
ST 版の forall s. にとても納得しました。それぞれの関数が任意のライフタイム (state thread?) (の array) に適用できるという感じですね
頂いた ST 版でエラーが出ました。 FlexibleContextsRankNTypes は有効ですが、他の言語拡張が足りなかったりしますか……?
    • Could not deduce (MArray (STUArray s) e (ST s))
        arising from a use of 'uarray'
      from the context: Ix i
        bound by the type signature for:
                   tabulateIO :: forall i e.
                                  Ix i =>
                                  (forall s.
                                   MArray (STUArray s) e (ST s) =>
                                   STUArray s i e -> i -> ST s e)
                                  -> (i, i) -> e -> UArray i e
        at /Path/To/atcoder-tessoku-hs/a19/Main.hs:65:1-139
    • In the first argument of 'runSTUArray', namely 'uarray'
      In the expression: runSTUArray uarray
      In an equation for 'tabulateIO':
          tabulateIO f bounds_ e0
            = runSTUArray uarray
            where
                uarray ::
                  forall s. MArray (STUArray s) e (ST s) => ST s (STUArray s i e)
                uarray
                  = do tbl <- ...
                       ....
   |
67 |   runSTUArray uarray
   |               ^^^^^^
( (追記: e を Int に置き換えたら通りました) 。また引数 f の型が推論されなかったので、割り切って tabulateST を諦めるのも良い気がしてきました。 Boilerplate と言うほどのコード量でもないですものね)
書き方悪くてすみません
ST 版はここまで書いて runSTUArrayeMArray (STUArray s) e (ST s) を満たすよって教えられなかった
ので、型検査通りませんでした:confounded:
あ、すみません!
型変数 e をやめて具体的にすれば通ると思います
通りました! DP がヌルヌル解ける……わけでもなかったので、 tabulateST は止めようと思います。ありがとうございます!
(ところで BOOTH などで kakkun61 さんのコンテンツのお世話になっております。その節もありがとうございます :pray: )
わあ、ありがとうございます!
@toyboot4e @kakkun61 今更ですが、次のコードが型チェック通りました:


{-# LANGUAGE QuantifiedConstraints, RankNTypes, FlexibleContexts, ScopedTypeVariables #-}

import 
import  (ST)
import Data.Array.Unboxed
import Control.Monad (forM_)

tabulateIO :: forall i e. (Ix i, forall s. MArray (STUArray s) e (ST s)) => (forall s. STUArray s i e -> i -> ST s e) -> (i, i) -> e -> UArray i e
tabulateIO f bounds_ e0 =
  runSTUArray uarray
  where
    uarray :: forall s. MArray (STUArray s) e (ST s) => ST s (STUArray s i e)
    uarray = do
      tbl <- newArray bounds_ e0 :: ST s (STUArray s i e)
      forM_ (range bounds_) $ \i -> do
        e <- f tbl i
        writeArray tbl i e
      return tbl
は~、なるほどです
:memo: :memo: (kakkun61 さん版 tabulateIO :: ..=> を 1 個にして QuantifiedConstraints を有効化)
ありがとうございます!! :sob: これを応用して、色々なパターンをシュッと書いていきたいと思います!
提出 してきました。型推論もバッチリです! :pray: