Zulip Chat Archive
Stream: general
Topic: namespace design issues vs. pedagogy
Kevin Sullivan (Aug 03 2018 at 13:02):
The context for the following is an effort to use Lean in teaching a lower-level undergraduate course. Students are easily confused. I'm trying to use namespaces in the usual way so that we can recapitulate the definitions of built-in types, such as bool, without getting name conflicts. It "works", but in the course of figuring out the details, I noted some inconsistencies that I myself found confusing and that students are likely to find confusing, too. I comment the code. The key lines are marked AAA through EEE.
In the outer environment I define a type, bar, with constructors p and q, and open its namespace. This models the built-in type, bool, the namespace of which is open by default. Then I define a namespace, foo. Within foo, I define bar again, in the same way. Everything else occurs within the foo namespace.
Inconsistency #1: In the command "#check bar", on the line marked (AAA), bar is interpreted as referring to foo.bar (as the little equality claim proves); but if you issue the command "open bar" (BBB), bar is interpreted as referring to _root_.bar (so the open command has no effect, as the _root_.bar namespace is already open). The same identifier is thus interpreted in two different ways in the same environment. This is kind of confusing and will be hard to explain to new students in CS.
Inconsistency #2: If you now open foo.bar, then you end up with two definitions of the constructor p. Not unexpectedly, you get error messages flagging an "ambiguous interpretation" in both the command, "#check p", at line (CCC) and in the statement of a simple theorem on line (DDD). Uncomment those two lines to see the error. And yet, when p is used as an argument to a function (line EEE), no ambiguity is flagged; rather it's resolved in favor of foo.bar.p from the inner namespace. So once again the same
identifier is interpreted inconsistently in the same environment. Again, explaining that is p flagged as erroneous when used in the statement of a theorem or in a #check command but not when used as an argument to a function will tend to confuse my students.
Is there a compelling reason for these behaviors? If so, can you briefly explain? If not, perhaps they can be fixed in some near future version?
== the code ==
inductive bar : Type
| p : bar
| q : bar
open bar
#check bar
#check p
namespace foo
inductive bar : Type
| p : bar
| q : bar
#check bar -- bar here refers to foo.bar (AAA)
theorem t1 : bar = foo.bar := rfl
#check p -- as expected, p still refers to _root_.bar.p
theorem t2 : p = _root_.bar.p := rfl
open bar -- bar here refers to _root_.bar! (BBB)
#check bar -- bar still refers to foo.bar
theorem t3 : bar = foo.bar := rfl
#check p -- p still refers to _root_.bar.p
theorem t4 : p = _root_.bar.p := rfl
open foo.bar -- need to use qualified name to open namespace
#check bar -- bar still refers to foo.bar
theorem t5 : bar = foo.bar := rfl
--#check p -- p is now ambiguous (CCC)
--theorem t6 : p = _root_.bar.p := rfl -- p is ambiguous (DDD)
def id(a: bar): bar := a
#check id p -- but in this expression, p is not ambiguous (EEE)
end foo
Johan Commelin (Aug 03 2018 at 13:03):
Kevin, you can use three backticks around your code to create a codeblock. If you write "lean" after the first set of backticks you get syntax highlighting! Like so
```lean <your code> -- nesting backticks is hard... somehow there are 4 backticks below. There should be only 3. ````
Kevin Sullivan (Aug 03 2018 at 13:03):
The context for the following is an effort to use Lean in teaching a lower-level undergraduate course. Students are easily confused. I'm trying to use namespaces in the usual way so that we can recapitulate the definitions of built-in types, such as bool, without getting name conflicts. It "works", but in the course of figuring out the details, I noted some inconsistencies that I myself found confusing and that students are likely to find confusing, too. I comment the code. The key lines are marked AAA through EEE.
In the outer environment I define a type, bar, with constructors p and q, and open its namespace. This models the built-in type, bool, the namespace of which is open by default. Then I define a namespace, foo. Within foo, I define bar again, in the same way. Everything else occurs within the foo namespace.
Inconsistency #1: In the command "#check bar", on the line marked (AAA), bar is interpreted as referring to foo.bar (as the little equality claim proves); but if you issue the command "open bar" (BBB), bar is interpreted as referring to _root_.bar (so the open command has no effect, as the _root_.bar namespace is already open). The same identifier is thus interpreted in two different ways in the same environment. This is kind of confusing and will be hard to explain to new students in CS.
Inconsistency #2: If you now open foo.bar, then you end up with two definitions of the constructor p. Not unexpectedly, you get error messages flagging an "ambiguous interpretation" in both the command, "#check p", at line (CCC) and in the statement of a simple theorem on line (DDD). Uncomment those two lines to see the error. And yet, when p is used as an argument to a function (line EEE), no ambiguity is flagged; rather it's resolved in favor of foo.bar.p from the inner namespace. So once again the same
identifier is interpreted inconsistently in the same environment. Again, explaining that is p flagged as erroneous when used in the statement of a theorem or in a #check command but not when used as an argument to a function will tend to confuse my students.Is there a compelling reason for these behaviors? If so, can you briefly explain? If not, perhaps they can be fixed in some near future version?
== the code ==
inductive bar : Type
| p : bar
| q : bar
open bar#check bar
#check pnamespace foo
inductive bar : Type
| p : bar
| q : bar#check bar -- bar here refers to foo.bar (AAA)
theorem t1 : bar = foo.bar := rfl
#check p -- as expected, p still refers to _root_.bar.p
theorem t2 : p = _root_.bar.p := rflopen bar -- bar here refers to _root_.bar! (BBB)
#check bar -- bar still refers to foo.bar
theorem t3 : bar = foo.bar := rfl
#check p -- p still refers to _root_.bar.p
theorem t4 : p = _root_.bar.p := rflopen foo.bar -- need to use qualified name to open namespace
#check bar -- bar still refers to foo.bar
theorem t5 : bar = foo.bar := rfl
--#check p -- p is now ambiguous (CCC)
--theorem t6 : p = _root_.bar.p := rfl -- p is ambiguous (DDD)def id(a: bar): bar := a
#check id p -- but in this expression, p is not ambiguous (EEE)
end foo
Ok, I guess in case #2, type inference is resolving the type of p. What about the first inconsistency?
Kevin Sullivan (Aug 03 2018 at 13:05):
Kevin, you can use three backticks around your code to create a codeblock. If you write "lean" after the first set of backticks you get syntax highlighting! Like so
inductive bar : Type | p : bar | q : bar open bar #check bar #check p namespace foo inductive bar : Type | p : bar | q : bar #check bar -- bar here refers to foo.bar (AAA) theorem t1 : bar = foo.bar := rfl #check p -- as expected, p still refers to _root_.bar.p theorem t2 : p = _root_.bar.p := rfl open bar -- bar here refers to _root_.bar! (BBB) #check bar -- bar still refers to foo.bar theorem t3 : bar = foo.bar := rfl #check p -- p still refers to _root_.bar.p theorem t4 : p = _root_.bar.p := rfl open foo.bar -- need to use qualified name to open namespace #check bar -- bar still refers to foo.bar theorem t5 : bar = foo.bar := rfl --#check p -- p is now ambiguous (CCC) --theorem t6 : p = _root_.bar.p := rfl -- p is ambiguous (DDD) def id(a: bar): bar := a #check id p -- but in this expression, p is not ambiguous (EEE) end foo
Simon Hudon (Aug 03 2018 at 13:45):
I think the take away from AAA through CCC is that when you use open
you must fully qualify the name that you use.
Simon Hudon (Aug 03 2018 at 13:51):
This might provide some more helpful details: https://leanprover.github.io/reference/other_commands.html#namespaces
Kevin Buzzard (Aug 03 2018 at 16:57):
I completely agree that this is confusing; I ran into this myself when trying to teach mathematicians how to build nat
with its addition etc. I tried to do everything within a namespace but could never get everything to be as straightforward as I wanted. My solution in the end was to simply give up trying to build my_namespace.nat
and as you can see at https://xenaproject.wordpress.com/2017/10/31/building-the-non-negative-integers-from-scratch/ I just called them xnat
instead (x for xena). I was even super-paranoid and made them in a xena
namespace. Now the trick is to make sure nobody opens nat
:-) and then I found that my problems had gone away. Within the xena
namespace I could define things like add_assoc
and because of this pile of preventative measures I could be fairly confident that nobody would be trying to use the default add_assoc
later on when proving add_comm
or whatever for xnat
. You might want to call my solution a workaround, but I took the practical viewpoint that it wasn't my job to teach people about namespaces -- they were mathematicians. You might be in a different situation of course.
Kevin Buzzard (Aug 03 2018 at 17:02):
PS if you start with
inductive bar : Type 7 | p : bar | q : bar open bar #check bar #check p namespace foo inductive bar : Type 4 | p : bar | q : bar
then it's a little easier to figure out which bar
is which when you're experimenting ;-)
Last updated: Dec 20 2023 at 11:08 UTC