Haskell-numpyの変形を再現


8

Haskellに入って、リストでnumpyの再形成のようなものを再現しようとしています。具体的には、フラットリストを指定して、n次元リストに再形成します。

import numpy as np

a = np.arange(1, 18)
b = a.reshape([-1, 2, 3])

# b = 
# 
# array([[[ 1,  2,  3],
#         [ 4,  5,  6]],
# 
#        [[ 7,  8,  9],
#         [10, 11, 12]],
# 
#        [[13, 14, 15],
#         [16, 17, 18]]])

固定インデックスで動作を再現することができました。例:

*Main> reshape23 [1..18]
[[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]],[[13,14,15],[16,17,18]]]

私のコードは:

takeWithRemainder :: (Integral n) => n -> [a] -> ([a], [a])
takeWithRemainder _ [] = ([], [])
takeWithRemainder 0 xs = ([], xs)
takeWithRemainder n (x:xs) = (x : taken, remaining)
                                where (taken, remaining) = takeWithRemainder (n-1) xs

chunks :: (Integral n) => n -> [a] -> [[a]]
chunks _ [] = []
chunks chunkSize xs = chunk : chunks chunkSize remainderOfList
                        where (chunk, remainderOfList) = takeWithRemainder chunkSize xs

reshape23 = chunks 2 . chunks 3

今、これを任意の形に一般化する方法を見つけることができないようです。私の元々のアイデアはフォールドをすることでした:

reshape :: (Integral n) => [n] -> [a] -> [b]
reshape ns list = foldr (\n acc -> (chunks n) . acc) id ns list

しかし、どのように処理しても、コンパイラーから常に型エラーが発生します。私の理解から、問題はある時点での型accid's ie a -> aであると推論され、フォールド内の関数のリストがすべて異なる(構成には互換性がありますが)型であるという事実が好きではないことです署名。これをフォールドの代わりに自分で再帰的に実装しようとすると、同じ問題が発生します。もともと私が意図していたので、これは私を混乱[b]reshapeスタンドインから何もすることができ、 『別の、解離したタイプ』のようにの型シグネチャ[[a]][[[[[a]]]]]

これはどうして間違っているのですか?私が意図した動作を実際に達成する方法はありますか、または最初にこの種の「動的」動作が必要なのは明らかに間違っていますか?

回答:


10

ここには、Pythonとは質的に異なる2つの詳細があります。最終的には、動的型付けと静的型付けに由来します。

最初に気づいたのは、チャンキングの各ステップで、結果の型が入力の型と異なることです。これはfoldr、特定のタイプの関数を想定しているため、を使用できないことを意味します。ただし、再帰を介してそれを行うことができます。

2番目の問題は少し明白ではありません。reshape関数の戻り値の型は、最初の引数が何であるかに依存します。同様に、最初の引数がの[2]場合、戻り値の型は[[a]]ですが、最初の引数がの場合[2, 3]、戻り値の型は[[[a]]]です。Haskellでは、すべての型がコンパイル時に認識されている必要があります。これはreshape、実行時に定義された最初の引数を関数が受け取ることができないことを意味します。つまり、最初の引数は型レベルでなければなりません。

タイプレベルの値はタイプ関数(別名「タイプファミリー」)を介して計算できますが、タイプだけではない(つまり、計算する値もある)ため、その自然な(または唯一の)メカニズムはタイプです。クラス。

だから、まずタイプクラスを定義しましょう:

class Reshape (dimensions :: [Nat]) from to | dimensions from -> to where
    reshape :: from -> to

クラスには3つのパラメータがあります。dimensions種類[Nat]は、目的のディメンションを表す、タイプレベルの数値配列です。from引数の型でありto、結果の型です。引数の型が常に[a]であることがわかっている場合でも、ここでは型変数として指定する必要があります。そうしないと、クラスのインスタンスがa引数と結果の間で同じに正しく一致できないためです。

さらに、このクラスには、とのdimensions from -> to両方を知っていれば、明確に判断できることを示す機能的な依存関係があります。dimensionsfromto

次に、基本ケース:dimentions空のリストの場合、関数は次のように低下​​しidます。

instance Reshape '[] [a] [a] where
    reshape = id

そして今、肉:再帰ケース。

instance (KnownNat n, Reshape tail [a] [b]) => Reshape (n:tail) [a] [[b]] where
    reshape = chunksOf n . reshape @tail
        where n = fromInteger . natVal $ Proxy @n

最初にreshape @tail、前の次元をチャンクする再帰呼び出しを行い、次に、現在の次元の値をチャンクサイズとして使用して、その結果をチャンクします。

chunksOfライブラリの関数splitを使用していることにも注意してください。自分で再定義する必要はありません。

試してみましょう:

λ reshape @ '[1] [1,2,3]          
[[1],[2],[3]]                     

λ reshape @ '[1,2] [1,2,3,4]        
[[[1,2]],[[3,4]]]                   

λ reshape @ '[2,3] [1..12]              
[[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]

λ reshape @ '[2,3,4] [1..24]                                                      
[[[[1,2,3,4],[5,6,7,8],[9,10,11,12]],[[13,14,15,16],[17,18,19,20],[21,22,23,24]]]]

参考までに、すべてのインポートと拡張を含む完全なプログラムを次に示します。

{-# LANGUAGE
    MultiParamTypeClasses, FunctionalDependencies, TypeApplications,
    ScopedTypeVariables, DataKinds, TypeOperators, KindSignatures,
    FlexibleInstances, FlexibleContexts, UndecidableInstances,
    AllowAmbiguousTypes
#-}

import Data.Proxy (Proxy(..))
import Data.List.Split (chunksOf)
import GHC.TypeLits (Nat, KnownNat, natVal)

class Reshape (dimensions :: [Nat]) from to | dimensions from -> to where
    reshape :: from -> to

instance Reshape '[] [a] [a] where
    reshape = id

instance (KnownNat n, Reshape tail [a] [b]) => Reshape (n:tail) [a] [[b]] where
    reshape = chunksOf n . reshape @tail
        where n = fromInteger . natVal $ Proxy @n

(..)部分はimport Data.Proxy (Proxy(..))何ですか?
Micha Wiedenmann

@MichaWiedenmann (..)は、すべてのデータ型コンストラクタと、場合によってはレコードフィールドをインポートすることを意味します。Proxyコンストラクタは1つしかないため、これは次と同等ですProxy(Proxy))
lehins

6

@Fyodor Soikinの回答は、実際の質問に対して完璧です。質問自体に少し問題があることを除いて。リストのリストは配列と同じではありません。Haskellには配列がなく、リストを処理せざるを得ないというのはよくある誤解です。

質問にタグが付けられ、arrayとの比較があるためnumpy、多次元配列のこの状況を処理する適切な回答を追加したいと思います。Haskellエコシステムには2つのアレイライブラリがあり、そのうちの1つはmassiv

reshape以下からのような機能numpyによって達成することができるresize'機能。

λ> 1 ... (18 :: Int)
Array D Seq (Sz1 18)
  [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 ]
λ> resize' (Sz (3 :> 2 :. 3)) (1 ... (18 :: Int))
Array D Seq (Sz (3 :> 2 :. 3))
  [ [ [ 1, 2, 3 ]
    , [ 4, 5, 6 ]
    ]
  , [ [ 7, 8, 9 ]
    , [ 10, 11, 12 ]
    ]
  , [ [ 13, 14, 15 ]
    , [ 16, 17, 18 ]
    ]
  ]

2
ここでも、私の回答と同じように、dimensions引数は型レベルです
Fyodor Soikin
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.