Closed null-a closed 3 years ago
I’m glad to see that you’re giving this project a spin!
I would indeed expect const x . f
to get rewritten to const x
, and there’s an explicit rule for doing so. Other rewrite rules can interfere in some cases. Does that rewrite raise concerns for you? Your example suggests an additional useful rule: const x &&& const y = const (x,y)
(or variant). From your definition, I’d expect the whole thing to get rewritten to const p
or unitArrow p
for a suitable p
.
Thanks @conal!
I would indeed expect const x . f to get rewritten to const x
Yes, me too. I think perhaps my Comp
constructor, which is left-to-right composition, was confusing things.
Either way, I've now simplified my example highlighting (what I think is) the problem, so let me share that with you:
I'm now transforming this function:
f :: () -> Bool
f () = prim 0.0
Where:
prim :: Double -> Bool
prim = unCcc Prim
... and Prim
is a constructor of a datatype:
data C :: * -> * -> * where
Comp :: C a b -> C b c -> C a c
Const :: Show b => b -> C a b
Prim :: C Double Bool
-- more constructors ...
(See Main.hs
and MyLib.hs
for the full code.)
This initially works as expected -- the function f
is transformed in to Comp (Const ?) (Prim)
. (Again, Comp
is left-to-right composition. Const ?
is a constant arrow, and the ?
just stems from my odd Show
instance.)
However, when I uncomment this innocuous seeming ConstCat
instance, the output changes from Comp (Const ?) (Prim)
to Const ?
. (As though the uncommenting had introduced a new rule f . const x = const x
.)
I hope that description makes sense?
I would indeed expect const x . f to get rewritten to const x
Yes, me too. I think perhaps my
Comp
constructor, which is left-to-right composition, was confusing things.
Thanks! I’m afraid I responded hurriedly and assumed Comp
was the other way around without following the source links you kindly provided.
However, when I uncomment this innocuous seeming
ConstCat
instance, the output changes fromComp (Const ?) (Prim)
toConst ?
. (As though the uncommenting had introduced a new rulef . const x = const x
.)I hope that description makes sense?
Indeed it does, now that I’m reading you more carefully.
I think I know what’s going on here (thanks to having been bitten similarly). With the “innocuous seeming ConstCat
instance” (for Bool
), you’re allowing const (prim 0.0)
to be translated to Const (prim 0.0)
, since prim 0.0 :: Bool
. Without that instance, the transformation for function application under lambdas gets applied instead, in which case, prim
and 0.0
get translated separately. One subtle point here is that you never evaluate prim 0.0
, because your show
is non-strict in the argument to Const
, so we see “Const ?
” rather than “Const
” and then an error from trying to evaluate unCcc Prim
.
Since you have a Show
constraint in your Const
constructor, how about changing the corresponding show
clause to show the argument? If my hunch is right, you’ll encounter the the unCcc
“oops” error
If my hunch is right, you’ll encounter the the unCcc “oops” error
You're right -- I made the change you suggested and I'm seeing exactly that.
Is there a way I can have both of the ConstCat
instances defined, and still see Comp (Const 0.0) Prim
rather than the error?
Is there a way I can have both of the
ConstCat
instances defined, and still seeComp (Const 0.0) Prim
rather than the error?
I don’t think so, since the ConstCat C Bool
instance exactly directs the plugin to insert Bool
-valued constants whenever \ x -> c
is encountered for Core expressions c :: Bool
not involving x
. Here, the expression c
is prim 0.0
, which is undefined.
If you want to define prim
for (->)
as well as for C
, you can do so by making prim
the method of a new class and give two instances. (Really, I think you’d need to do as in the tedious AltCat
-vs-Category
split to avoid premature method inlining. Sigh.)
What motivates you to want the Core C Bool
instance and yet not constant-fold prim 0.0
at compile time?
I’m very much up for reconsidering some of the choices I made around constant folding criteria. Discussion & suggestions most welcome.
Thanks for the insights, this is helpful!
I'm imagining that my C a b
represents a probability distribution over values of type b
, parameterized by (or conditioned on) a value of type a
.
My Prim :: C Double Bool
could represent a primitive Bernoulli distribution -- a distribution over {True,False}
parametized by the probability for True
. I'll call this Bern
from now on.
I'm imagining using your plugin to transform a Haskell function in to a value of my type C a b
, which I'll then interpret further. For concreteness, we might imagine interpreting a C a b
as a sampler, i.e. as something like a function a -> Random b
.
I've updated my example to implement exactly this. (Main.hs
, MyLib.hs
)
As a simple example, I'd like to be able to describe the uniform distribution over Bool
like so:
uniform :: () -> Bool
uniform () = bern 0.5
(Here bern = unCcc Bern :: Double -> Bool
is my primitive Bernoulli distribution.)
My main
function demonstrates this working as I had hoped. uniform
is transformed to Comp (Const 0.5) Bern
, which I then turn into a sampler, which I can run in order to draw a bunch of samples. So far so good!
What motivates you to want the Core C Bool instance
Here's a contrived example of why I'd like to add a ConstCat
instance for Bool
to what I have:
contrivedUniform :: () -> Bool
contrivedUniform () = if bern 0.5 then False else True
After running this through the plugin and then sample
, I'm hoping this will also produce a sampler for the uniform distribution over Bool
.
I believe I need the ConstCat
instance for the Bool
literals, but once I add that I'm seeing the Oops: unCcc' called
error, presumably for the reasons we're discussing here.
and yet not constant-fold prim 0.0 at compile time?
I'm struggling to answer this precisely. Instead, I can only gesture towards things which are surprising or confusing to me. (Sorry I'm not able to do better!)
I think my expectation was that a Haskell function such as uniform
would first be transformed into something like an expression in abstract categorical vocabulary, with the mapping to the target category happen after.
So uniform = const (bern 0.5)
in Haskell code becomes bern . const 0.5
in abstract categorical vocabulary, which becomes Comp (Const 0.5) Bern
when mapped to my target category.
It seems important here that even though the Haskell expression involves application, the expression in the categorical vocab doesn't, since my target category (values of type C a b
) doesn't have application.
I became confused when you pointed out that the plugin might directly rewrite const (bern 0.5)
to Const (bern 0.5)
. This seems problematic for my case, since I don't see how we could then translate bern 0.5
, given that I don't have application in my target category.
So perhaps the problem is that I have assumed I can use this without a ClosedCat
instance, and that's not the case?
Thanks for these explanations, which help me see your intentions more clearly. I’m quite interested in seeing compiling-to-categories used for probabilistic computation.
I understand now why there can’t be a definition of bern
for (->)
. I’ve run into some similar situations and am happy to see another, as I suspect there’s something interesting and valuable to learn from these cases.
I believe I need the
ConstCat
instance for theBool
literals, but […]
Maybe you do need this instance, but I don’t yet see why. Perhaps so that you can translate other constant boolean expressions. Even if you don’t need a ConstCat C Bool
instance, however, I worry that you will run into the same difficulty with types other than Bool
, particularly Double
.
It seems important here that even though the Haskell expression involves application, the expression in the categorical vocab doesn’t, since my target category (values of type
C a b
) doesn’t have application.I became confused when you pointed out that the plugin might directly rewrite
const (bern 0.5)
toConst (bern 0.5)
. This seems problematic for my case, since I don’t see how we could then translatebern 0.5
, given that I don’t have application in my target category.
Really, the plugin would rewrite toCcc (const (bern 0.5))
to Const (bern 0.5)
and then leave the bern 0.5
alone, since it’s not under a toCcc
. Your category would not know or care about (nor be able to examine) the (Haskell/Core) expression that denotes its argument (crucial for tractable, sound reasoning).
So perhaps the problem is that I have assumed I can use this without a
ClosedCat
instance, and that’s not the case?
The topic of closure may indeed arise, but the plugin only introduces that vocabulary when it’s available (i.e., for closed target categories). Closure is handy and leads to more compact translations, but I’ve tried not to rely on it.
I’m intrigued with your sample
definition. It looks like there’s another category wanting to be made explicit here, wrapping a -> Rand b
. Then another functor that relates this more semantic category with your current more syntactic category, with both steps being simpler and possibly more useful functors.
Remember that instances Category
should really be categories, including the laws, despite Haskell’s type system being too weak to catch violations. Have you considered whether your C
really is a category (i.e., satisfies the laws)? (Likewise for the Category
subclasses.)
I’m quite interested in seeing compiling-to-categories used for probabilistic computation.
Great! There are one or two things in that general area that I hope to try, once I'm over this initial hurdle.
Have you considered whether your C really is a category
That's a good point. To side-step this, we could just consider your Syn
category, since (having just tried it) I now see that it has similarly confusing (to me) behavior. That prompts the following question: Can I add primitive arrows (beyond those provided by the type classes) to e.g. Syn
?
For example, I can define:
primitive :: Syn Double Bool
primitive = app0 "primitive"
... and successfully combine that with other values of type Syn
. e.g. I can write:
foo :: Syn () Bool
foo = primitive . const 0.5
-- λ> foo
-- primitive . const 0.5
My expectation was that I would be able to generate the value foo
using the plugin, with something like:
primitive' :: Double -> Bool
primitive' = unCcc primitive
main :: IO ()
main = print $ toCcc @Syn (\_ -> primitive' 0.5)
... but if I run that I get Oops: unCcc' called
, which makes me wonder whether a misunderstanding of how to introduce new primitives is my un-doing?
(If fact, I can't even do toCcc @Syn primitive'
, since that gives me Oops: toCcc' called
. Urghh, I think I might be out of my depth!)
(The full code for this is here.)
Have you considered whether your
C
really is a categoryThat’s a good point. To side-step this,
The meaning and correctness of compiling to categories depends on having a genuine category as target and a homomorphism that relates mathematical functions to that category. For this fundamental reason, I encourage facing these questions early and directly. Moreover, these questions often have lovely answers from which much else follows inevitably.
That said, I’m very interested in exploring cases in which we don’t know how to apply this neat, well-defined homomorphism story, and probabilistic programming may well be one. I wonder: can we find a compelling & precise specification for what we’re trying to accomplish here, as a homomorphism or otherwise?
we could just consider your
Syn
category, since (having just tried it) I now see that it has similarly confusing (to me) behavior. That prompts the following question: Can I add primitive arrows (beyond those provided by the type classes) to e.g.Syn
?
The plugin knows about only a few of the class methods. Most of the translation is simple and generic, and works by assuming that toCcc
is a homomorphism for every class operation to which it’s applied if that operation is instantiated at (->)
. The (->)
operation is replaced by the corresponding operation for the target category if it’s available. You can easily add additional classes. For this technique, the catch is that the methods must exist for (->)
in order for toCcc
to be applicable and thus for the plugin to translate to your desired category/interpretation. We can give a bogus (->)
instance, but then we’ll get the same constant-folding error. The other technique I’ve experimented with is unCcc
, which purports to convert from your category to (->)
. As you may have surmised, unCcc
has no actual implementation. Its purpose is only to cancel out with a toCcc
applied to it, thanks to a rewrite rule. Two things can go wrong, however:
toCcc
gets applied, or a toCcc
to a category other than the one used with the unCcc
, in which cases cancellation won’t happen.foo
example with Syn
.We can explore two paths for resolution:
unCcc
. Without a specification, I don’t know that we can do better than suppressing symptoms of our lack of understanding as those symptoms arise.My sympathies are very much with the first path, leading to insight and correctness proofs rather than merely ignorance of incorrectness; while the second path is what I see as dominating the contemporary practice of programming. If our values & goals align, I’d be interested in collaborating or at least learning from each other.
Thanks for this, it's really helpful.
My sympathies are very much with the first path
I'm sympathetic too. It's an appreciation of your general approach that brought me here. I'd love to read "The simple essence of probabilistic programming", and I'd be keen to help bring that into existence if there's anything at all I can contribute.
I concede that the approach I have mind (which prompted me to open this issue) is rather less ambitious than that though -- partly because I doubt I'm up to the challenge of applying your method to this domain myself, and partly because I have an immediate need I might plausibly be able to address with this. Nevertheless, what I have may be of some interest to you, so when I get chance I'll pull some notes together and share them with you. It would be great to hear your thoughts if you have the time to take a look, and perhaps they could be a starting point for further discussion. (It will take me a while to get to this, since this isn't the day job, sadly.)
Thanks again for taking the time to discuss this with me, it's greatly appreciated.
I'm playing with a toy example of defining my own categorical instances in order to get a feel for using the plugin, and I'm running into unexpected behavior. I have a data type to record the categorical expressions generated, and I'm trying to transform a simple function to that. Note that my data type has a constructor that isn't covered by any of the categorical type classes, so I'm using
unCcc
with that.With the code as is, this all works fine. For example, the function:
... is transformed into:
... which is correct. The unexpected part, is that when I uncomment this
ConstCat
instance and re-runstack run
, the output changes to:... which is not what I expect -- the
Prim
arrows have disappeared.I'd be grateful to hear any suggestions about what might be happening and how I might fix this.
Here's the output I see with the plugin debug flags set on the erroneous case:
P.S. Thanks for sharing this wonderful work!