-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathContinuous.lean
More file actions
457 lines (363 loc) · 20.1 KB
/
Continuous.lean
File metadata and controls
457 lines (363 loc) · 20.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
/-
Copyright (c) 2017 Johannes Hölzl. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Johannes Hölzl, Mario Carneiro, Jeremy Avigad
-/
module
public import Mathlib.Topology.ClusterPt
/-!
# Continuity in topological spaces
For topological spaces `X` and `Y`, a function `f : X → Y` and a point `x : X`,
`ContinuousAt f x` means `f` is continuous at `x`, and global continuity is
`Continuous f`. There is also a version of continuity `PContinuous` for
partially defined functions.
## Tags
continuity, continuous function
-/
@[expose] public section
open Set Filter Topology
variable {X Y Z : Type*}
open TopologicalSpace
-- The curly braces are intentional, so this definition works well with simp
-- when topologies are not those provided by instances.
theorem continuous_def {_ : TopologicalSpace X} {_ : TopologicalSpace Y} {f : X → Y} :
Continuous f ↔ ∀ s, IsOpen s → IsOpen (f ⁻¹' s) :=
⟨fun hf => hf.1, fun h => ⟨h⟩⟩
variable [TopologicalSpace X] [TopologicalSpace Y] [TopologicalSpace Z]
variable {f : X → Y} {s : Set X} {x : X} {y : Y}
theorem IsOpen.preimage (hf : Continuous f) {t : Set Y} (h : IsOpen t) :
IsOpen (f ⁻¹' t) :=
hf.isOpen_preimage t h
lemma Equiv.continuous_symm_iff (e : X ≃ Y) : Continuous e.symm ↔ IsOpenMap e := by
simp_rw [continuous_def, ← Equiv.image_eq_preimage_symm, IsOpenMap]
lemma Equiv.isOpenMap_symm_iff (e : X ≃ Y) : IsOpenMap e.symm ↔ Continuous e := by
simp_rw [← Equiv.continuous_symm_iff, Equiv.symm_symm]
theorem continuous_congr {g : X → Y} (h : ∀ x, f x = g x) :
Continuous f ↔ Continuous g :=
.of_eq <| congrArg _ <| funext h
theorem Continuous.congr {g : X → Y} (h : Continuous f) (h' : ∀ x, f x = g x) : Continuous g :=
continuous_congr h' |>.mp h
theorem ContinuousAt.tendsto (h : ContinuousAt f x) :
Tendsto f (𝓝 x) (𝓝 (f x)) :=
h
theorem continuousAt_def : ContinuousAt f x ↔ ∀ A ∈ 𝓝 (f x), f ⁻¹' A ∈ 𝓝 x :=
Iff.rfl
theorem continuousAt_congr {g : X → Y} (h : f =ᶠ[𝓝 x] g) :
ContinuousAt f x ↔ ContinuousAt g x := by
simp only [ContinuousAt, tendsto_congr' h, h.eq_of_nhds]
theorem ContinuousAt.congr {g : X → Y} (hf : ContinuousAt f x) (h : f =ᶠ[𝓝 x] g) :
ContinuousAt g x :=
(continuousAt_congr h).1 hf
theorem ContinuousAt.preimage_mem_nhds {t : Set Y} (h : ContinuousAt f x)
(ht : t ∈ 𝓝 (f x)) : f ⁻¹' t ∈ 𝓝 x :=
h ht
/-- If `f x ∈ s ∈ 𝓝 (f x)` for continuous `f`, then `f y ∈ s` near `x`.
This is essentially `Filter.Tendsto.eventually_mem`, but infers in more cases when applied. -/
theorem ContinuousAt.eventually_mem {f : X → Y} {x : X} (hf : ContinuousAt f x) {s : Set Y}
(hs : s ∈ 𝓝 (f x)) : ∀ᶠ y in 𝓝 x, f y ∈ s :=
hf hs
/-- If a function `f` tends to somewhere other than `𝓝 (f x)` at `x`,
then `f` is not continuous at `x`
-/
lemma not_continuousAt_of_tendsto {f : X → Y} {l₁ : Filter X} {l₂ : Filter Y} {x : X}
(hf : Tendsto f l₁ l₂) [l₁.NeBot] (hl₁ : l₁ ≤ 𝓝 x) (hl₂ : Disjoint (𝓝 (f x)) l₂) :
¬ ContinuousAt f x := fun cont ↦
(cont.mono_left hl₁).not_tendsto hl₂ hf
theorem ClusterPt.map {lx : Filter X} {ly : Filter Y} (H : ClusterPt x lx)
(hfc : ContinuousAt f x) (hf : Tendsto f lx ly) : ClusterPt (f x) ly :=
(NeBot.map H f).mono <| hfc.tendsto.inf hf
/-- See also `interior_preimage_subset_preimage_interior`. -/
theorem preimage_interior_subset_interior_preimage {t : Set Y} (hf : Continuous f) :
f ⁻¹' interior t ⊆ interior (f ⁻¹' t) :=
interior_maximal (preimage_mono interior_subset) (isOpen_interior.preimage hf)
theorem continuous_iff_preimage_interior_subset_interior_preimage :
Continuous f ↔ ∀ s, f ⁻¹' (interior s) ⊆ interior (f ⁻¹' s) where
mp h s := preimage_interior_subset_interior_preimage h
mpr h := ⟨fun s hs ↦ subset_interior_iff_isOpen.mp <| by grw [← h, hs.interior_eq]⟩
@[continuity]
theorem continuous_id : Continuous (id : X → X) :=
continuous_def.2 fun _ => id
-- This is needed due to reducibility issues with the `continuity` tactic.
@[continuity, fun_prop]
theorem continuous_id' : Continuous (fun (x : X) => x) := continuous_id
theorem Continuous.comp {g : Y → Z} (hg : Continuous g) (hf : Continuous f) :
Continuous (g ∘ f) :=
continuous_def.2 fun _ h => (h.preimage hg).preimage hf
-- This is needed due to reducibility issues with the `continuity` tactic.
@[continuity, fun_prop]
theorem Continuous.comp' {g : Y → Z} (hg : Continuous g) (hf : Continuous f) :
Continuous (fun x => g (f x)) := hg.comp hf
@[fun_prop]
theorem Continuous.iterate {f : X → X} (h : Continuous f) (n : ℕ) : Continuous f^[n] :=
Nat.recOn n continuous_id fun _ ihn => ihn.comp h
nonrec theorem ContinuousAt.comp {g : Y → Z} (hg : ContinuousAt g (f x))
(hf : ContinuousAt f x) : ContinuousAt (g ∘ f) x :=
hg.comp hf
@[fun_prop]
theorem ContinuousAt.comp' {g : Y → Z} {x : X} (hg : ContinuousAt g (f x))
(hf : ContinuousAt f x) : ContinuousAt (fun x => g (f x)) x := ContinuousAt.comp hg hf
/-- See note [comp_of_eq lemmas] -/
theorem ContinuousAt.comp_of_eq {g : Y → Z} (hg : ContinuousAt g y)
(hf : ContinuousAt f x) (hy : f x = y) : ContinuousAt (g ∘ f) x := by subst hy; exact hg.comp hf
theorem Continuous.tendsto (hf : Continuous f) (x) : Tendsto f (𝓝 x) (𝓝 (f x)) :=
((nhds_basis_opens x).tendsto_iff <| nhds_basis_opens <| f x).2 fun t ⟨hxt, ht⟩ =>
⟨f ⁻¹' t, ⟨hxt, ht.preimage hf⟩, Subset.rfl⟩
/-- A version of `Continuous.tendsto` that allows one to specify a simpler form of the limit.
E.g., one can write `continuous_exp.tendsto' 0 1 exp_zero`. -/
theorem Continuous.tendsto' (hf : Continuous f) (x : X) (y : Y) (h : f x = y) :
Tendsto f (𝓝 x) (𝓝 y) :=
h ▸ hf.tendsto x
@[fun_prop]
theorem Continuous.continuousAt (h : Continuous f) : ContinuousAt f x :=
h.tendsto x
theorem Continuous.preimage_mem_nhds {t : Set Y} (h : Continuous f)
(ht : t ∈ 𝓝 (f x)) : f ⁻¹' t ∈ 𝓝 x :=
h.continuousAt.preimage_mem_nhds ht
theorem continuous_iff_continuousAt : Continuous f ↔ ∀ x, ContinuousAt f x :=
⟨Continuous.tendsto, fun hf => continuous_def.2 fun _U hU => isOpen_iff_mem_nhds.2 fun x hx =>
hf x <| hU.mem_nhds hx⟩
@[fun_prop]
theorem continuousAt_const : ContinuousAt (fun _ : X => y) x :=
tendsto_const_nhds
@[continuity, fun_prop]
theorem continuous_const : Continuous fun _ : X => y :=
continuous_iff_continuousAt.mpr fun _ => continuousAt_const
theorem Filter.EventuallyEq.continuousAt (h : f =ᶠ[𝓝 x] fun _ => y) :
ContinuousAt f x :=
(continuousAt_congr h).2 tendsto_const_nhds
theorem continuous_of_const (h : ∀ x y, f x = f y) : Continuous f :=
continuous_iff_continuousAt.mpr fun x =>
Filter.EventuallyEq.continuousAt <| Eventually.of_forall fun y => h y x
theorem continuousAt_id : ContinuousAt id x :=
continuous_id.continuousAt
@[fun_prop]
theorem continuousAt_id' (y) : ContinuousAt (fun x : X => x) y := continuousAt_id
theorem ContinuousAt.iterate {f : X → X} (hf : ContinuousAt f x) (hx : f x = x) (n : ℕ) :
ContinuousAt f^[n] x :=
Nat.recOn n continuousAt_id fun _n ihn ↦ ihn.comp_of_eq hf hx
theorem continuous_iff_isClosed : Continuous f ↔ ∀ s, IsClosed s → IsClosed (f ⁻¹' s) :=
continuous_def.trans <| compl_surjective.forall.trans <| by
simp only [isOpen_compl_iff, preimage_compl]
theorem IsClosed.preimage (hf : Continuous f) {t : Set Y} (h : IsClosed t) :
IsClosed (f ⁻¹' t) :=
continuous_iff_isClosed.mp hf t h
theorem mem_closure_image (hf : ContinuousAt f x)
(hx : x ∈ closure s) : f x ∈ closure (f '' s) :=
mem_closure_of_frequently_of_tendsto
((mem_closure_iff_frequently.1 hx).mono fun _ => mem_image_of_mem _) hf
theorem Continuous.closure_preimage_subset (hf : Continuous f) (t : Set Y) :
closure (f ⁻¹' t) ⊆ f ⁻¹' closure t := by
rw [← (isClosed_closure.preimage hf).closure_eq]
exact closure_mono (preimage_mono subset_closure)
theorem Continuous.frontier_preimage_subset (hf : Continuous f) (t : Set Y) :
frontier (f ⁻¹' t) ⊆ f ⁻¹' frontier t :=
diff_subset_diff (hf.closure_preimage_subset t) (preimage_interior_subset_interior_preimage hf)
/-- If a continuous map `f` maps `s` to `t`, then it maps `closure s` to `closure t`. -/
protected theorem Set.MapsTo.closure {t : Set Y} (h : MapsTo f s t)
(hc : Continuous f) : MapsTo f (closure s) (closure t) := by
simp only [MapsTo, mem_closure_iff_clusterPt]
exact fun x hx => hx.map hc.continuousAt (tendsto_principal_principal.2 h)
/-- See also `IsClosedMap.closure_image_eq_of_continuous`. -/
theorem image_closure_subset_closure_image (h : Continuous f) :
f '' closure s ⊆ closure (f '' s) :=
((mapsTo_image f s).closure h).image_subset
theorem closure_image_closure (h : Continuous f) :
closure (f '' closure s) = closure (f '' s) :=
Subset.antisymm
(closure_minimal (image_closure_subset_closure_image h) isClosed_closure)
(closure_mono <| image_mono subset_closure)
theorem closure_subset_preimage_closure_image (h : Continuous f) :
closure s ⊆ f ⁻¹' closure (f '' s) :=
(mapsTo_image _ _).closure h
theorem continuous_iff_image_closure_subset_closure_image :
Continuous f ↔ ∀ s, f '' closure s ⊆ closure (f '' s) where
mp h s := image_closure_subset_closure_image h
mpr h := continuous_iff_isClosed.mpr fun s hs ↦ isClosed_of_closure_subset <| by
grw [image_subset_iff.mp <| h <| f ⁻¹' s, image_preimage_subset, hs.closure_subset]
theorem map_mem_closure {t : Set Y} (hf : Continuous f)
(hx : x ∈ closure s) (ht : MapsTo f s t) : f x ∈ closure t :=
ht.closure hf hx
/-- If a continuous map `f` maps `s` to a closed set `t`, then it maps `closure s` to `t`. -/
theorem Set.MapsTo.closure_left {t : Set Y} (h : MapsTo f s t)
(hc : Continuous f) (ht : IsClosed t) : MapsTo f (closure s) t :=
ht.closure_eq ▸ h.closure hc
theorem Filter.Tendsto.lift'_closure (hf : Continuous f) {l l'} (h : Tendsto f l l') :
Tendsto f (l.lift' closure) (l'.lift' closure) :=
tendsto_lift'.2 fun s hs ↦ by
filter_upwards [mem_lift' (h hs)] using (mapsTo_preimage _ _).closure hf
theorem tendsto_lift'_closure_nhds (hf : Continuous f) (x : X) :
Tendsto f ((𝓝 x).lift' closure) ((𝓝 (f x)).lift' closure) :=
(hf.tendsto x).lift'_closure hf
/-!
### Function with dense range
-/
section DenseRange
variable {α ι : Type*} (f : α → X) (g : X → Y)
variable {f : α → X} {s : Set X}
/-- A surjective map has dense range. -/
theorem Function.Surjective.denseRange (hf : Function.Surjective f) : DenseRange f := fun x => by
simp [hf.range_eq]
theorem denseRange_id : DenseRange (id : X → X) :=
Function.Surjective.denseRange Function.surjective_id
theorem denseRange_iff_closure_range : DenseRange f ↔ closure (range f) = univ :=
dense_iff_closure_eq
theorem DenseRange.closure_range (h : DenseRange f) : closure (range f) = univ :=
h.closure_eq
@[simp]
lemma denseRange_subtype_val {p : X → Prop} : DenseRange (@Subtype.val _ p) ↔ Dense {x | p x} := by
simp [DenseRange]
theorem Dense.denseRange_val (h : Dense s) : DenseRange ((↑) : s → X) :=
denseRange_subtype_val.2 h
theorem Continuous.range_subset_closure_image_dense {f : X → Y} (hf : Continuous f)
(hs : Dense s) : range f ⊆ closure (f '' s) := by
rw [← image_univ, ← hs.closure_eq]
exact image_closure_subset_closure_image hf
/-- The image of a dense set under a continuous map with dense range is a dense set. -/
theorem DenseRange.dense_image {f : X → Y} (hf' : DenseRange f) (hf : Continuous f)
(hs : Dense s) : Dense (f '' s) :=
(hf'.mono <| hf.range_subset_closure_image_dense hs).of_closure
/-- If `f` has dense range and `s` is an open set in the codomain of `f`, then the image of the
preimage of `s` under `f` is dense in `s`. -/
theorem DenseRange.subset_closure_image_preimage_of_isOpen (hf : DenseRange f) (hs : IsOpen s) :
s ⊆ closure (f '' (f ⁻¹' s)) := by
rw [image_preimage_eq_inter_range]
exact hf.open_subset_closure_inter hs
/-- If a continuous map with dense range maps a dense set to a subset of `t`, then `t` is a dense
set. -/
theorem DenseRange.dense_of_mapsTo {f : X → Y} (hf' : DenseRange f) (hf : Continuous f)
(hs : Dense s) {t : Set Y} (ht : MapsTo f s t) : Dense t :=
(hf'.dense_image hf hs).mono ht.image_subset
/-- Composition of a continuous map with dense range and a function with dense range has dense
range. -/
theorem DenseRange.comp {g : Y → Z} {f : α → Y} (hg : DenseRange g) (hf : DenseRange f)
(cg : Continuous g) : DenseRange (g ∘ f) := by
rw [DenseRange, range_comp]
exact hg.dense_image cg hf
nonrec theorem DenseRange.nonempty_iff (hf : DenseRange f) : Nonempty α ↔ Nonempty X :=
range_nonempty_iff_nonempty.symm.trans hf.nonempty_iff
theorem DenseRange.nonempty [h : Nonempty X] (hf : DenseRange f) : Nonempty α :=
hf.nonempty_iff.mpr h
/-- Given a function `f : X → Y` with dense range and `y : Y`, returns some `x : X`. -/
noncomputable def DenseRange.some (hf : DenseRange f) (x : X) : α :=
Classical.choice <| hf.nonempty_iff.mpr ⟨x⟩
nonrec theorem DenseRange.exists_mem_open (hf : DenseRange f) (ho : IsOpen s) (hs : s.Nonempty) :
∃ a, f a ∈ s :=
exists_range_iff.1 <| hf.exists_mem_open ho hs
theorem DenseRange.mem_nhds (h : DenseRange f) (hs : s ∈ 𝓝 x) :
∃ a, f a ∈ s :=
let ⟨a, ha⟩ := h.exists_mem_open isOpen_interior ⟨x, mem_interior_iff_mem_nhds.2 hs⟩
⟨a, interior_subset ha⟩
end DenseRange
library_note «continuity lemma statement» /--
The library contains many lemmas stating that functions/operations are continuous. There are many
ways to formulate the continuity of operations. Some are more convenient than others.
Note: for the most part this note also applies to other properties
(`Measurable`, `Differentiable`, `ContinuousOn`, ...).
### The traditional way
As an example, let's look at addition `(+) : M → M → M`. We can state that this is continuous
in different definitionally equal ways (omitting some typing information)
* `Continuous (fun p ↦ p.1 + p.2)`;
* `Continuous (Function.uncurry (+))`;
* `Continuous ↿(+)`. (`↿` is notation for recursively uncurrying a function)
However, lemmas with this conclusion are not nice to use in practice because
1. They confuse the elaborator. The following example fails, because of limitations in the
elaboration process.
```
variable {M : Type*} [Add M] [TopologicalSpace M] [ContinuousAdd M]
example : Continuous (fun x : M ↦ x + x) :=
continuous_add.comp _
-- This example used to fail, but would be accepted if you wrote is as
-- `continuous_add.comp (continuous_id.prodMk continuous_id :)`.
example : Continuous (fun x : M ↦ x + x) :=
continuous_add.comp (continuous_id.prodMk continuous_id)
```
2. If the operation has more than 2 arguments, they are impractical to use, because in your
application the arguments in the domain might be in a different order or associated differently.
### The convenient way
A much more convenient way to write continuity lemmas is like `Continuous.add`:
```
Continuous.add {f g : X → M} (hf : Continuous f) (hg : Continuous g) :
Continuous (f + g)
```
The conclusion can be `Continuous (fun x ↦ f x + g x)`, which is definitionally equal.
This has the following advantages
* It supports projection notation, so is shorter to write.
* `Continuous.add _ _` is recognized correctly by the elaborator and gives useful new goals.
* It works generally, since the domain is a variable.
(Having a domain `Y × Z` would be less convenient in general.)
As an example for a unary operation, we have `Continuous.neg`.
```
Continuous.neg {f : X → G} (hf : Continuous f) : Continuous (-f)
```
For unary functions, the elaborator is not confused when applying the traditional lemma
(like `continuous_neg`), but it's still convenient to have the short version available (compare
`hf.neg.neg.neg` with `continuous_neg.comp <| continuous_neg.comp <| continuous_neg.comp hf`).
As a harder example, consider an operation of the following type:
```
def strans {x : F} (γ γ' : Path x x) (t₀ : I) : Path x x
```
The precise definition is not important, only its type.
The correct continuity principle for this operation is something like this:
```
{f : X → F} {γ γ' : ∀ x, Path (f x) (f x)} {t₀ s : X → I}
(hγ : Continuous ↿γ) (hγ' : Continuous ↿γ')
(ht : Continuous t₀) (hs : Continuous s) :
Continuous (fun x ↦ strans (γ x) (γ' x) (t x) (s x))
```
Note that *all* arguments of `strans` are indexed over `X`, even the basepoint `x`, and the last
argument `s` that arises since `Path x x` has a coercion to `I → F`. The paths `γ` and `γ'` (which
are unary functions from `I`) become binary functions in the continuity lemma.
### Summary
* Make sure that your continuity lemmas are stated in the most general way, and in a convenient
form. That means that:
- The conclusion has a variable `X` as domain (not something like `Y × Z`);
- Wherever possible, all point arguments `c : Y` are replaced by functions `c : X → Y`;
- All `n`-ary function arguments are replaced by `n+1`-ary functions
(`f : Y → Z` becomes `f : X → Y → Z`);
- All (relevant) arguments have continuity assumptions, and perhaps there are additional
assumptions needed to make the operation continuous;
- The function in the conclusion is fully applied.
* These remarks are mostly about the format of the *conclusion* of a continuity lemma.
In assumptions it's fine to state that a function with more than 1 argument is continuous using
`↿` or `Function.uncurry`.
### Functions with discontinuities
In some cases, you want to work with discontinuous functions, and in certain expressions they are
still continuous. For example, consider the fractional part of a number, `Int.fract : ℝ → ℝ`.
In this case, you want to add conditions to when a function involving `fract` is continuous, so you
get something like this: (assumption `hf` could be weakened, but the important thing is the shape
of the conclusion)
```
lemma ContinuousOn.comp_fract {X Y : Type*} [TopologicalSpace X] [TopologicalSpace Y]
{f : X → ℝ → Y} {g : X → ℝ} (hf : Continuous ↿f) (hg : Continuous g) (h : ∀ s, f s 0 = f s 1) :
Continuous (fun x ↦ f x (fract (g x)))
```
With `ContinuousAt` you can be even more precise about what to prove in case of discontinuities,
see e.g. `ContinuousAt.comp_div_cases`.
-/
library_note «comp_of_eq lemmas» /--
Lean's elaborator has trouble elaborating applications of lemmas that state that the composition of
two functions satisfy some property at a point, like `ContinuousAt.comp` / `ContDiffAt.comp` and
`ContMDiffWithinAt.comp`. The reason is that a lemma like this looks like
`ContinuousAt g (f x) → ContinuousAt f x → ContinuousAt (g ∘ f) x`.
Since Lean's elaborator elaborates the arguments from left-to-right, when you write `hg.comp hf`,
the elaborator will try to figure out *both* `f` and `g` from the type of `hg`. It tries to figure
out `f` just from the point where `g` is continuous. For example, if `hg : ContinuousAt g (a, x)`
then the elaborator will assign `f` to the function `Prod.mk a`, since in that case `f x = (a, x)`.
This is undesirable in most cases where `f` is not a variable. There are some ways to work around
this, for example by giving `f` explicitly, or to force Lean to elaborate `hf` before elaborating
`hg`, but this is annoying.
Another better solution is to reformulate composition lemmas to have the following shape
`ContinuousAt g y → ContinuousAt f x → f x = y → ContinuousAt (g ∘ f) x`.
This is even useful if the proof of `f x = y` is `rfl`.
The reason that this works better is because the type of `hg` doesn't mention `f`.
Only after elaborating the two `ContinuousAt` arguments, Lean will try to unify `f x` with `y`,
which is often easy after having chosen the correct functions for `f` and `g`.
Here is an example that shows the difference:
```
example [TopologicalSpace X] [TopologicalSpace Y] {x₀ : X} (f : X → X → Y)
(hf : ContinuousAt (Function.uncurry f) (x₀, x₀)) :
ContinuousAt (fun x ↦ f x x) x₀ :=
-- hf.comp (continuousAt_id.prod continuousAt_id) -- type mismatch
-- hf.comp_of_eq (continuousAt_id.prod continuousAt_id) rfl -- works
```
-/