web-dev-qa-db-ja.com

明確にするために新しいタイプ/データを作成するのは悪いフォームですか?

私はこのようなことをするのが悪いかどうか知りたいです:

data Alignment = LeftAl | CenterAl | RightAl
type Delimiter = Char
type Width     = Int

setW :: Width -> Alignment -> Delimiter -> String -> String

このようなものではなく:

setW :: Int -> Char -> Char -> String -> String

これらの型を作り直すことは、より明確なコードと引き換えに数行しか使わないことを知っています。ただし、複数の関数にタイプDelimiterを使用すると、このモジュールをインポートしたり、後でコードを読んだりする人にとって、これははるかに明確になります。

私はHaskellに比較的慣れていないので、この種のものの良い習慣は何なのかわかりません。これが良い考えではない場合、またはより明確に改善できるものが好ましい場合、それは何でしょうか?

17
BryceTheGrand

タイプエイリアスを使用していますが、コードの読みやすさはわずかです。ただし、型の安全性を高めるには、newtypeではなくtypeを使用することをお勧めします。このような:

data Alignment = LeftAl | CenterAl | RightAl
newtype Delimiter = Delimiter { unDelimiter :: Char }
newtype Width     = Width { unWidth :: Int }

setW :: Width -> Alignment -> Delimiter -> String -> String

newtypeの追加のラッピングとアンラッピングを扱います。しかし、コードはさらなるリファクタリングに対してより堅牢になります。 このスタイルガイド は、ポリモーフィック型を特殊化する場合にのみtypeを使用することをお勧めします。

20
Shersh

私はその悪い形は考慮しませんが、明らかに、Haskellコミュニティ全体については話しません。言語機能は、私が知る限り、その特定の目的のために存在します。コードを読みやすくすることです。

さまざまな「コア」ライブラリでの型エイリアスの使用例を見つけることができます。たとえば、Readクラスはこのメソッドを定義します。

readList :: ReadS [a]

ReadS型は単なる型エイリアスです

type ReadS a = String -> [(a, String)]

別の例は Forest type in Data.Tree です:

type Forest a = [Tree a]

Shershが指摘するように、newtype宣言で新しい型をラップすることもできます。これは、元の型をなんらかの方法で制約する必要がある場合(たとえば、 スマートコンストラクター を使用する場合)またはOrphanインスタンスを作成せずに型に機能を追加する場合(通常、QuickCheckを定義することです) Arbitraryインスタンスを、他の方法ではそのようなインスタンスに付属しないタイプに変換します)。

14
Mark Seemann

newtypeを使用すると、基になる型と同じ表現で新しい型が作成されますが、置換できません—good形式と見なされます。これは primitive obsession を避けるための安価な方法であり、Haskellでは関数の引数の名前がシグネチャに表示されないため、Haskellにとって特に便利です。

Newtypesは、便利なtypeclassインスタンスをぶら下げる場所にもなります。

Haskellのnewtypeは至る所に存在するため、時間の経過とともに言語はそれらを操作するためのいくつかのツールとイディオムを獲得しています。

  • Coercible newtypeコンストラクターがスコープ内にある場合に、newtypesとその基になる型の間の変換を簡略化する「魔法の」型クラス。関数の実装でボイラープレートを回避するのによく役立ちます。

    ghci> coerce (Sum (5::Int)) :: Int

    ghci> coerce [Sum (5::Int)] :: [Int]

    ghci> coerce ((+) :: Int -> Int -> Int) :: Identity Int -> Identity Int -> Identity Int

  • alafoldMapなどの関数で使用する可能性のある新しいタイプの選択を簡単にするイディオム(さまざまなパッケージで実装)。

    ala Sum foldMap [1,2,3,4 :: Int] :: Int

  • GeneralizedNewtypeDeriving 。基本タイプで使用可能なインスタンスに基づいて、新しいタイプのインスタンスを自動派生させるための拡張。

  • DerivingVia より一般的な拡張機能で、他のいくつかで使用可能なインスタンスに基づいて、新しいタイプのインスタンスを自動導出します同じ基本型を持つnewtype。

10
danidiaz

注意すべき重要な点の1つは、AlignmentCharの違いは、明快さの問題だけでなく、正確さの問題です。あなたのAlignment型は、Charが持っている多くの住民とは対照的に、有効な配置が3つしかないという事実を表します。これを使用することで、無効な値や操作の問題を回避し、警告がオンになっている場合にGHCが不完全なパターン一致について情報を提供できるようにします。

同義語については、意見が異なります。個人的には、typeのような小さな型のIntの同義語は、厳密に同じものの異なる名前を追跡させることにより、認知負荷を高めることができると感じています。つまり、 leftaroundaboutは重要なポイントになります この種の同義語は、ソリューションのプロトタイピングの初期段階で、具体的な表現の詳細について必ずしも気にしたくない場合に役立ちます。ドメインオブジェクトに採用します。

(ここでのtypeに関する注釈は、主にnewtypeには当てはまりません。ただし、ユースケースは異なります。ただし、typeは、同じこと、newtypeはフィアットによって別のことを紹介します。これは驚くほど強力な動きになる可能性があります-詳細については danidiazの回答 を参照してください。)

6
duplode

間違いなく良いです。ここに別の例を示します。このデータ型にいくつかのopがあるとします。

data Form = Square Int | Rectangle Int Int | EqTriangle Int

perimeter :: Form -> Int
perimeter (Square s)      = s * 4
perimeter (Rectangle b h) = (b * h) * 2
perimeter (EqTriangle s)  = s * 3

area :: Form -> Int
area (Square s)      = s ^ 2
area (Rectangle b h) = (b * h)
area (EqTriangle s)  = (s ^ 2) `div` 2 

次に、円を追加するとします。

data Form = Square Int | Rectangle Int Int | EqTriangle Int | Cicle Int

その操作を追加します。

perimeter (Cicle r )      = pi * 2 * r

area (Cicle r)       = pi * r ^ 2

あまり良くないですね。 Floatを使用したい... FloatのすべてのIntを変更する必要があります

data Form = Square Double | Rectangle Double Double | EqTriangle Double | Cicle Double


area :: Form -> Double

perimeter :: Form -> Double

しかし、明確にするために、そして再利用のために、タイプを使用するとどうなるでしょうか。

data Form = Square Side | Rectangle Side Side | EqTriangle Side | Cicle Radius

type Distance = Int
type Side = Distance
type Radius = Distance
type Area = Distance

perimeter :: Form -> Distance
perimeter (Square s)      = s * 4
perimeter (Rectangle b h) = (b * h) * 2
perimeter (EqTriangle s)  = s * 3
perimeter (Cicle r )      = pi * 2 * r

area :: Form -> Area
area (Square s)      = s * s
area (Rectangle b h) = (b * h)
area (EqTriangle s)  = (s * 2) / 2
area (Cicle r)       = pi * r * r

これにより、コードの1行だけを変更してタイプを変更できます。DistanceをIntにしたい場合は、それだけを変更します

perimeter :: Form -> Distance
...

totalDistance :: [Form] -> Distance
totalDistance = foldr (\x rs -> perimeter x + rs) 0

距離をフロートにしたいので、変更します。

type Distance = Float

Intに変更したい場合は、関数をいくつか調整する必要がありますが、それは他の問題です。