Skip to content

Why not From a b implies TryFrom a b or why class From exists at all? #169

Description

@betafcc

Apologies in advance, this might boil down to a newbie Haskell advice request, but as far as I can see, the question is valid enough.

I arrived on a similar implementation of a Rust-inspired TryFrom and while refining it with AI, it pointed me to this project as reference. A big difference that I was surprised to find was the stated

You should not have both a From instance and a TryFrom instance for the same pair of types.

Also mentioned in #43 like an obvious law.

But I wonder, why is that? I can't help but read this as

Total functions are not a subset of partial functions

Sure, I can see that with both class TryFrom and class From, we want to prevent the possibility of their instances having differing implementations, but Rust enforces it with a blanket instance/impl (albeit with a From/Into indirection):
https://doc.rust-lang.org/src/core/convert/mod.rs.html#825-833

// Infallible conversions are semantically equivalent to fallible conversions
// with an uninhabited error type.
const impl<T, U> TryFrom<U> for T
where
    U: [const] Into<T>,
{
    type Error = Infallible;

Which in Haskell direct translation would be:

instance From a b => TryFrom a b where

I reckon there are problems with this, for one, this would be {-# OVERLAPPABLE #-} and not good for a library design choice, but keeping close to Rust implementation of associated Error type and Infallible case, this seemed as the natural GHC-sound equivalent:

class TryFrom source target where
    type TryFromError source target :: Type
    tryFrom :: source -> Either (TryFromError source target) target

type From source target = (TryFrom source target, TryFromError source target ~ Void)

Ie. having From being a refinement instead of it's own class. Was there a strong reason to not take a similar path? The dowside I can think is the use of Void being needed, which I'm not sure of how idiomatic it is in Haskell.

Still it feels pretty solid for my reading:

A total conversion is a special case of partial conversion

The motivation

My actual use-cases are somewhat weak, but hopefully illustrative enough.

I've been using ghci ocasionally as a shell and runhaskell for everyday scripts to help me get more Haskell exp.
In that context, I really miss the convenience of loose union parameters like in Python or TS, so I've been using Rust conventions to achieve similar ergonomics on my local utils.

Eg. TS fetch signature:

function fetch(input: string | URL | Request): Promise<Response>

This allows:

fetch("https://example.com") // I can *try* making a GET request from that
fetch(new URL(...)) // I can *definitely* make a GET request from that

In Rust, supporting this is pretty common, by using Into/TryInto, but with Witch.TryFrom:

fetch :: (TryFrom a Request) => a -> IO Response

-- instance TryFrom String Request
fetch "https://example.com" -- Sure, I can *try* making a GET request from that
-- instance From Url Request
fetch Url{...} -- Type Error: sorry, not fallible enough

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions