如何根据Haskell中的输入值限制类型?

时间:2016-09-27 02:47:36

标签: haskell types functional-programming

我是Haskell的新手。对不起,如果这个问题有明显的答案。

我有

data Tmp = Foo Int
         | Bar Int
         | Baz Int

data Test = A Tmp Tmp

构造函数A Tmp Tmp可以使用Tmp的任何构造函数,除了 A (Baz i) (Baz j)其中ijIntTmp。我有什么办法吗? 如果第一个A Tmp Tmp已经Baz,则限制Tmp中的第二个Baz免于package com.example.malik.shoutout; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import com.google.android.gms.drive.Drive; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; import com.google.android.gms.location.LocationListener; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; public abstract class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mGoogleApiClient = new GoogleApiClient.Builder(this) .addApi(Drive.API) .addScope(Drive.SCOPE_FILE) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .build(); setContentView(R.layout.activity_main); } }

1 个答案:

答案 0 :(得分:13)

答案取决于您希望如何强制执行限制:在运行时或编译时。

要在运行时强制执行限制,您可以添加一个检查限制的函数(例如makeA),然后调用构造函数。这样一个函数可以执行一些操作,然后调用构造函数也称为智能构造函数。如果您只从模块中导出智能构造函数makeA而不是真正的构造函数A,则可以确保其他模块使用智能构造函数,因此始终会检查限制。

示例:

module Test (Tmp (Foo, Bar, Baz), Test (), makeA) where
  data Tmp
     = Foo Int
     | Bar Int
     | Baz Int

  data Test = A Tmp Tmp

  makeA :: Tmp -> Tmp -> Tmp
  makeA (Baz _) (Baz _) = error "makeA: two baz problem"
  makeA tmp1 tmp2 = A tmp1 tmp2

此技术的好处是您根本不必更改数据类型。缺点是限制仅在运行时强制执行。

要在编译时强制执行限制,您需要以某种方式更改数据类型。当前数据类型的问题在于类型检查器无法区分由FooBar构造的值以及由Baz构造的值。对于类型检查器,这些都是Tmp值,因此类型检查器不能强制某些Tmp值正常而其他值不正确。所以我们必须更改数据类型以编码类型中Tmp值的“Bazness”。

类型中编码Bazness的一个选项是重构Tmp,如下所示:

data TmpNotBaz
  = Foo Int
  | Bar Int

data Tmp
  = NotBaz TmpNotBaz
  | Baz Int

现在很明显,TmpNotBaz类型的值不能是Baz,但类型Tmp的值可以是Baz。这个想法的好处是它只使用基本的Haskell功能。一个小缺点是您需要将NotBaz的调用放入您的代码中。一个主要的缺点是,我们仍然不能直接表达“A如果另一个不是”Baz可以A的一个论点的想法。我们必须编写data Test = A1 TmpNotBaz Tmp | A2 Tmp TmpNotBaz 的多个版本:

A1

现在,我们可以根据需要选择A2A (Baz ...) (Baz ...)来表达我们想要的所有值,并且我们无法再根据需要表达A (Foo 1) (Foo 2)。此解决方案的一个问题是,过去有多种表示形式,例如A1 (Foo 1) (NotBaz (Foo 2))A2 (NotBaz (Foo 1)) (Foo 2)Tmp都代表此值。

您可以尝试使用此类数据类型的结构,并创建适合您情况的版本。

类型中对Bazness进行编码的另一个选择是将一些类型级别的信息注释到{-# LANGUAGE GADTs, TypeFamilies, DataKinds #-} data Bazness = IsBaz | NotBaz data BothBazOrNot = BothBaz | NotBothBaz type family AreBothBaz (b1 :: Bazness) (b2 :: Bazness) :: BothBazOrNot where AreBothBaz 'IsBaz 'IsBaz = 'BothBaz AreBothBaz _ _ = 'NotBothBaz data Tmp (b :: Bazness) :: * where Foo :: Int -> Tmp 'NotBaz Bar :: Int -> Tmp 'NotBaz Baz :: Int -> Tmp 'IsBaz data Test where A :: AreBothBaz b1 b2 ~ 'NotBothBaz => Tmp b1 -> Tmp b2 -> Test 类型,并使用类型级编程来推断这种类型 - 级别信息。这个想法的缺点是它使用更高级的Haskell功能。实际上,有许多新兴方法可以做这种事情,并且不清楚哪些最终将被视为“标准”高级Haskell。也就是说,这是一种方法:

Foo

请注意构造函数BarBazIsBaz的类型签名是如何讨论构造函数是否创建了NotBazA的内容。以及b1的类型签名如何讨论某些b2NotBothBaz选项,以便A (Foo 1) (Bar 2)

使用此代码,我们可以编写以下表达式:

  • A (Foo 1) (Baz 2)
  • A (Baz 1) (Bar 2)
  • A (Baz 1) (Baz 2)

但如果我们尝试编写Couldn't match type 'BothBaz with 'NotBothBaz arising from a use of A In the expression: A (Baz 1) (Baz 2) ,则类型检查器会抱怨:

A

因此类型检查器发现在这种情况下,BothBaz的参数是A,但我们注释NotBothBaz的类型只接受{{1}的参数},因此类型检查器会抱怨BothBazNotBothBaz不同。