6

Let's say I have a type like this:

data Foo = Bar String | Baz | Qux String

I want to have a function like this:

get : Foo -> String
get (Bar s) = s
get (Qux s) = s

As written, this compiles, but it's not total, as there are missing cases; in other words, get Baz is treated like a hole rather than as an expression that doesn't typecheck.

I want to replace that Foo in the type signature of get with something that specifies that the value must be either a Bar or a Qux. How can I express this subset of the Foo type?

Sam Estep
  • 12,974
  • 2
  • 37
  • 75

4 Answers4

4

You could also mix the two approaches (by Kim Stiebel and Anton Trunov) and construct a helper data type. The type OnlyBarAndQux can only be constructed with values of Bar and Qux. For idris it is then possible to automatically infer a proof if this is the case when invoking get:

module FooMe

data Foo = Bar String | Baz | Qux String

data OnlyBarAndQux: Foo -> Type where
    BarEy: OnlyBarAndQux (Bar s)
    QuxEx: OnlyBarAndQux (Qux s)

||| get a string from a Bar or Qux
total
get: (f: Foo) -> {auto prf : OnlyBarAndQux f} -> String
get (Bar s) {prf = BarEy} = s
get (Qux s) {prf = QuxEx} = s

-- Tests

test1: get $ Bar "hello" = "hello"
test1 = Refl

test2: get $ Qux "hello" = "hello"
test2 = Refl

-- does not compile
-- test3: get $ Baz = "hello"
Eduardo Pareja Tobes
  • 3,060
  • 1
  • 18
  • 19
Markus
  • 1,293
  • 9
  • 10
  • This looks like exactly what I want! Can you explain a bit more about the code in your definition of the `OnlyBarAndQux` type? I'm not very familiar with the `data`...`where` syntax. – Sam Estep Aug 22 '17 at 18:56
  • 1
    The idea is that you describe a type which can only be constructed (the constructors are after the where clause) by giving it values for Foo which are a Bar s or a Qux s. OnlyBarAndQux is a dependent type (depending on a value of Foo). So it is perfectly legal to write the type OnlyBarAndQux Baz, but it is impossible to construct it. That is what is being used in the auto prf construct. For further information have a look at the idris tutorial and the Type driven development book. – Markus Aug 23 '17 at 07:44
3

I'd follow the approach taken in the std library for List head, for example. This is basically what Markus wrote plus using Dec for witnessing that a Foo being not Baz is decidable:

%default total

data Foo = Bar String | Baz | Qux String

data NotBaz : Foo -> Type where
  IsBar: NotBaz(Bar z)
  IsQux: NotBaz(Qux z)

Uninhabited (NotBaz Baz) where
  uninhabited _ impossible

notBaz : (f : Foo) -> Dec (NotBaz f)
notBaz Baz      = No absurd
notBaz (Bar s)  = Yes IsBar
notBaz (Qux s)  = Yes IsQux

get: (f : Foo) -> {auto ok : NotBaz f} -> String
get (Bar s) { ok = IsBar } = s
get (Qux s) { ok = IsQux } = s

s: String
s = get (Bar "bar")

Some comments about this:

  1. Do not use just a predicate a -> Bool for working with a subset type of a; create a view like NotBaz above. See the Idris tutorial on views, this post, and this answer for context.
  2. Use Dec whenever possible instead of equality. Intutitively, you will be able to use Dec for predicates on types for which you can explicitly decide the truth of the predicate: see notBaz above.
  3. auto implicit arguments can help reducing the visual/cognitive clutter at usage site
Eduardo Pareja Tobes
  • 3,060
  • 1
  • 18
  • 19
  • Just for my understanding: wouldn't you need an additional case in the Uninhabated case as well, that it is impossible for `NotBaz Baz` to be `IsQux`? – Markus Aug 17 '17 at 08:13
  • not really; see for example [this answer](https://stackoverflow.com/a/32948013/614394). You just need a proof of `NotBaz Baz` being empty. – Eduardo Pareja Tobes Aug 17 '17 at 10:30
  • actually I think it's cleaner to just use an underscore there, edited. – Eduardo Pareja Tobes Aug 17 '17 at 10:31
  • @EduardoParejaTobes Thanks for the expansion of Markus's answer! Could you give an example of a case where `notBaz` would be useful/necessary? I don't think I entirely understand its value from your current answer. – Sam Estep Aug 22 '17 at 19:00
2

There is more than one way to do this, but the easiest is probably to make Foo a type constructor that takes a parameter indicating whether it's a Foo with a String in it or not. In this example I have used a Bool as the parameter:

%default total

data Foo : Bool -> Type where
  Bar : String -> Foo True -- a Foo constructed with Bar will have type Foo True
  Baz : Foo False -- and a Foo constructed with Baz will have type Foo False
  Qux : String -> Foo True

get : Foo True -> String
get (Bar s) = s
get (Qux s) = s
Kim Stebel
  • 41,826
  • 12
  • 125
  • 142
  • 2
    this approach requires changing `Foo` which is not good and might be impossible, as when `Foo` is imported from a library. In fact, it is not strictly a solution to defining `get` on the *provided* `Foo` type. – Eduardo Pareja Tobes Aug 16 '17 at 08:40
2

I'd go with Kim Stebel's answer (if changing Foo is an option, as observed by @Eduardo Pareja Tobes), but I'd like to show another way. You can use a subset type, which is the same thing as dependent pair:

total
get : (f ** Not (f = Baz)) -> String
get (f ** pf) with (f)
  get (f ** _)      | (Bar s) = s                      -- this is as before
  get (f ** contra) | Baz = void $ contra Refl         -- a contradictory case
  get (f ** _)      | (Qux s) = s                      -- this is as before

(f ** Not (f = Baz)) can be translated as "some f of type Foo, but not Baz".

To call get you need to provide a dependent pair of an element of type Foo and a proof that it is not equal to Baz, like so:

s : String
s = get (Bar "bar" ** \Refl impossible)
Anton Trunov
  • 15,074
  • 2
  • 23
  • 43