Zulip Chat Archive

Stream: lean4

Topic: Export smart constructor but also the type


cognivore (Sep 14 2022 at 18:38):

In X.lean:

private structure Name (x : String) where
  val : String := x
  deriving Repr

def isIdChar (x : Char) : Bool :=
  x.isAlphanum || "_.+-*/\\^~=<>!?@#$%&|:'`".data.elem x

def mkName (xs : String) : Option (Name xs) :=
  let xs' := xs.data
  if xs'.length == 0 then
    .none
  else
    if List.foldl (fun a x => if a then isIdChar x else false) true xs.data then
      .some {}
    else
      .none

in Main.lean:

import X
open X

def sameName (_n₁ : Name x) (_n₂ : Name x) : Name "Україна" := {}

This fails because Main can't refer to type Name. However, if I drop private, then I can construct an "invalid" type Name "Україна".

How would you approach this tradeoff?

Leonardo de Moura (Sep 14 2022 at 19:10):

You can use a private constructor.

structure Name (x : String) where
  private mk ::
  val : String := x
  deriving Repr

You may also tag val as private if you want.
BTW, I just pushed a fix for the { .. } since it was not checking whether the constructor was private or not.
It is also important to keep in mind that meta-programming can be used to access private names.

Leonardo de Moura (Sep 14 2022 at 19:11):

https://github.com/leanprover/lean4/commit/ef9127487ad9ed4ffc5dea4694f24a5a7fa820c4

Gabriel Ebner (Sep 14 2022 at 19:28):

So is the idea private fields/constructors should not be accessible unless you manually write a Expr.const `_private.. ..? There are so many ways around it that I always thought of private as more of a namespacing thing (like protected), and we certainly use it that way in core (to hide auxiliary definitions from the global namespace).

Around the private constructor:

  • ⟨..⟩
  • {..} (you've already fixed that one)
  • by constructor

Around the private projections:

  • let ⟨val⟩ := x; val
  • by cases val; assumption

Leonardo de Moura (Sep 14 2022 at 21:10):

So is the idea private fields/constructors should not be accessible unless you manually write a Expr.const `_private.. ..?

Yes, the goal is to support scenarios like the one @cognivore described.

There are so many ways around it that I always thought of private as more of a namespacing thing (like protected), and we certainly use it that way in core (to hide auxiliary definitions from the global namespace).

I will fix them. I thought I had fixed {..} in the past, and I was surprised when it did not work this morning.

Around the private constructor: ⟨..⟩

This is one it is easy to fix.

by constructor

Not sure about this one. The scenarios I had in mind are for people using Lean as a programming language. If people start using the tactic framework for coding we may want to support this one too.

Similar for projections.

Mario Carneiro (Sep 15 2022 at 02:29):

Just to add on what Gabriel said: Although we can maybe patch some holes in being able to peer through private, I think you should not ever expect it to become as air tight as privacy in ML or Rust because the kernel / defeq can see through privacy. You should use opaque if you want to block this, but be aware that opaque is not scope-sensitive, so you have to wrap everything behind the privacy barrier in a structure which is then passed to opaque. See for example src4#FloatSpec.

For this particular pattern (using private to uphold invariants), there is a superior solution which is simply to put the invariants in the type:

namespace X

def isIdChar (x : Char) : Bool :=
  x.isAlphanum || "_.+-*/\\^~=<>!?@#$%&|:'`".data.elem x

structure Name (x : String) where
  private mk ::
  length_ne_zero : x.length  0
  all_isIdChar : x.data.all isIdChar
  deriving Repr

def mkName (xs : String) : Option (Name xs) :=
  let xs' := xs.data
  if h₁ : xs'.length = 0 then
    none
  else
    if h₂ : xs.data.all isIdChar then
      some { length_ne_zero := h₁, all_isIdChar := h₂ }
    else
      none

end X
open X

def sameName (_n₁ : Name x) (_n₂ : Name x) : Name "Україна" := {} -- fail

That way, even if the user manages to get past the privacy barrier using tricks, they still have to prove the same invariants that your smart constructor did. In fact, with this pattern it's probably not even necessary to hide the constructor - users are allowed to construct the thing directly as long as they prove the invariants, and the smart constructor is available if they don't want to.


Last updated: Dec 20 2023 at 11:08 UTC