Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved feature ergonomics #128

Merged
merged 4 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cardano-api/cardano-api.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ library internal
Cardano.Api.Eras
Cardano.Api.Error
Cardano.Api.Feature
Cardano.Api.Feature.ConwayEraOnwards
Cardano.Api.Feature.ShelleyToBabbageEra
Cardano.Api.Fees
Cardano.Api.Genesis
Cardano.Api.GenesisParameters
Expand Down
27 changes: 19 additions & 8 deletions cardano-api/gen/Test/Gen/Cardano/Api/Typed.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}

{-# OPTIONS_GHC -Wno-deprecations #-}

module Test.Gen.Cardano.Api.Typed
( genFeatureValueInEra
( genFeaturedInEra
, genMaybeFeaturedInEra

, genAddressByron
, genAddressInEra
Expand Down Expand Up @@ -122,7 +125,6 @@ import Cardano.Api hiding (txIns)
import qualified Cardano.Api as Api
import Cardano.Api.Byron (KeyWitness (ByronKeyWitness),
WitnessNetworkIdOrByronAddress (..))
import Cardano.Api.Feature
import Cardano.Api.Script (scriptInEraToRefScript)
import Cardano.Api.Shelley (GovernancePoll (..), GovernancePollAnswer (..), Hash (..),
KESPeriod (KESPeriod),
Expand Down Expand Up @@ -732,16 +734,25 @@ genTxBody era = do
Left err -> fail (displayError err)
Right txBody -> pure txBody

-- | Generate a 'FeatureValue' for the given 'CardanoEra' with the provided generator.
genFeatureValueInEra :: ()
-- | Generate a 'Featured' for the given 'CardanoEra' with the provided generator.
genFeaturedInEra :: ()
=> Alternative f
=> feature era
-> f a
-> f (Featured feature era a)
genFeaturedInEra witness gen =
Featured witness <$> gen

-- | Generate a 'Featured' for the given 'CardanoEra' with the provided generator.
genMaybeFeaturedInEra :: ()
=> FeatureInEra feature
=> Alternative f
=> f a
-> CardanoEra era
-> f (FeatureValue feature era a)
genFeatureValueInEra gen =
featureInEra (pure NoFeatureValue) $ \witness ->
pure NoFeatureValue <|> fmap (FeatureValue witness) gen
-> f (Maybe (Featured feature era a))
genMaybeFeaturedInEra gen =
featureInEra (pure Nothing) $ \witness ->
pure Nothing <|> fmap Just (genFeaturedInEra witness gen)

genTxScriptValidity :: CardanoEra era -> Gen (TxScriptValidity era)
genTxScriptValidity era = case txScriptValiditySupportedInCardanoEra era of
Expand Down
43 changes: 2 additions & 41 deletions cardano-api/internal/Cardano/Api/Certificate.hs
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,6 @@ module Cardano.Api.Certificate (
-- * Data family instances
AsType(..),

-- * GADTs for Conway/Shelley differences
ShelleyToBabbageEra(..),
ConwayEraOnwards(..),

-- * Internal functions
filterUnRegCreds,
selectStakeCredential,
Expand All @@ -79,7 +75,8 @@ import Cardano.Api.Address
import Cardano.Api.DRepMetadata
import Cardano.Api.EraCast
import Cardano.Api.Eras
import Cardano.Api.Feature
import Cardano.Api.Feature.ConwayEraOnwards
import Cardano.Api.Feature.ShelleyToBabbageEra
import Cardano.Api.Governance.Actions.VotingProcedure
import Cardano.Api.HasTypeProxy
import Cardano.Api.Keys.Praos
Expand Down Expand Up @@ -316,42 +313,6 @@ data DRepMetadataReference =
-- Constructor functions
--

data ConwayEraOnwards era where
ConwayEraOnwardsConway :: ConwayEraOnwards ConwayEra

deriving instance Show (ConwayEraOnwards era)
deriving instance Eq (ConwayEraOnwards era)

instance FeatureInEra ConwayEraOnwards where
featureInEra no yes = \case
ByronEra -> no
ShelleyEra -> no
AllegraEra -> no
MaryEra -> no
AlonzoEra -> no
BabbageEra -> no
ConwayEra -> yes ConwayEraOnwardsConway

data ShelleyToBabbageEra era where
ShelleyToBabbageEraShelley :: ShelleyToBabbageEra ShelleyEra
ShelleyToBabbageEraAllegra :: ShelleyToBabbageEra AllegraEra
ShelleyToBabbageEraMary :: ShelleyToBabbageEra MaryEra
ShelleyToBabbageEraAlonzo :: ShelleyToBabbageEra AlonzoEra
ShelleyToBabbageEraBabbage :: ShelleyToBabbageEra BabbageEra

deriving instance Show (ShelleyToBabbageEra era)
deriving instance Eq (ShelleyToBabbageEra era)

instance FeatureInEra ShelleyToBabbageEra where
featureInEra no yes = \case
ByronEra -> no
ShelleyEra -> yes ShelleyToBabbageEraShelley
AllegraEra -> yes ShelleyToBabbageEraAllegra
MaryEra -> yes ShelleyToBabbageEraMary
AlonzoEra -> yes ShelleyToBabbageEraAlonzo
BabbageEra -> yes ShelleyToBabbageEraBabbage
ConwayEra -> no

data StakeAddressRequirements era where
StakeAddrRegistrationConway
:: ConwayEraOnwards era
Expand Down
40 changes: 40 additions & 0 deletions cardano-api/internal/Cardano/Api/Eras.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ module Cardano.Api.Eras
, InAnyCardanoEra(..)
, CardanoLedgerEra

-- * FeatureInEra
, FeatureInEra(..)
, maybeFeatureInEra
, featureInShelleyBasedEra

-- * Deprecated aliases
, Byron
, Shelley
Expand Down Expand Up @@ -71,10 +76,14 @@ import Ouroboros.Consensus.Shelley.Eras as Consensus (StandardAllegra,

import Control.DeepSeq
import Data.Aeson (FromJSON (..), ToJSON, toJSON, withText)
import Data.Kind
import qualified Data.Text as Text
import Data.Type.Equality (TestEquality (..), (:~:) (Refl))
import Data.Typeable (Typeable)

-- ----------------------------------------------------------------------------
-- Eras

-- | A type used as a tag to distinguish the Byron era.
data ByronEra

Expand Down Expand Up @@ -124,6 +133,37 @@ instance HasTypeProxy ConwayEra where
data AsType ConwayEra = AsConwayEra
proxyToAsType _ = AsConwayEra

-- ----------------------------------------------------------------------------
-- FeatureInEra

-- | A class for producing values for features that are supported in some eras
-- but not others.
class FeatureInEra (feature :: Type -> Type) where
-- | Determine the value to use for a feature in a given 'CardanoEra'.
-- Note that the negative case is the first argument, and the positive case is the second as per
-- the 'either' function convention.
featureInEra :: ()
=> a -- ^ Value to use if the feature is not supported in the era
-> (feature era -> a) -- ^ Function to get thealue to use if the feature is supported in the era
-> CardanoEra era -- ^ Era to check
-> a -- ^ The value to use

maybeFeatureInEra :: ()
=> FeatureInEra feature
=> CardanoEra era -- ^ Era to check
-> Maybe (feature era) -- ^ The feature if supported in the era
maybeFeatureInEra = featureInEra Nothing Just

-- | Determine the value to use for a feature in a given 'ShelleyBasedEra'.
featureInShelleyBasedEra :: ()
=> FeatureInEra feature
=> a
-> (feature era -> a)
-> ShelleyBasedEra era
-> a
featureInShelleyBasedEra no yes =
featureInEra no yes . shelleyBasedToCardanoEra

-- ----------------------------------------------------------------------------
-- Deprecated aliases
--
Expand Down
90 changes: 19 additions & 71 deletions cardano-api/internal/Cardano/Api/Feature.hs
Original file line number Diff line number Diff line change
@@ -1,98 +1,46 @@
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}

{-# OPTIONS_GHC -Wno-deprecations #-}

module Cardano.Api.Feature
( FeatureValue (..)
, FeatureInEra(..)
, featureInShelleyBasedEra
, valueOrDefault
, asFeatureValue
, asFeatureValueInShelleyBasedEra
, isFeatureValue
( Featured (..)
, asFeaturedInEra
, asFeaturedInShelleyBasedEra
) where

import Cardano.Api.Eras

import Data.Kind

-- | A class for features that are supported in some eras but not others.
class FeatureInEra (feature :: Type -> Type) where
-- | Determine the value to use for a feature in a given 'CardanoEra'.
-- Note that the negative case is the first argument, and the positive case is the second as per
-- the 'either' function convention.
featureInEra :: ()
=> a -- ^ Value to use if the feature is not supported in the era
-> (feature era -> a) -- ^ Function to get thealue to use if the feature is supported in the era
-> CardanoEra era -- ^ Era to check
-> a -- ^ The value to use

instance FeatureInEra ShelleyBasedEra where
featureInEra no yes = \case
ByronEra -> no
ShelleyEra -> yes ShelleyBasedEraShelley
AllegraEra -> yes ShelleyBasedEraAllegra
MaryEra -> yes ShelleyBasedEraMary
AlonzoEra -> yes ShelleyBasedEraAlonzo
BabbageEra -> yes ShelleyBasedEraBabbage
ConwayEra -> yes ShelleyBasedEraConway

-- | Determine the value to use for a feature in a given 'ShelleyBasedEra'.
featureInShelleyBasedEra :: ()
=> FeatureInEra feature
=> a
-> (feature era -> a)
-> ShelleyBasedEra era
-> a
featureInShelleyBasedEra no yes = featureInEra no yes . shelleyBasedToCardanoEra

-- | A value of type @'FeatureValue' feature era a@ is either:
data FeatureValue feature era a where
-- | A value is available for this feature in this era
FeatureValue
-- | A value only if the feature is supported in this era
data Featured feature era a where
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm skeptical about introducing another abstraction as we are in the process of handing over part of cardano-api to the ledger team. We don't want additional churn as we have an October target we need to hit. Where do you envision this being used?

Copy link
Collaborator Author

@newhoggy newhoggy Jul 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm anticipating that we it will be used in the CLI parser.

For example we currently don't have a good way to say some value that's passed into the run function from the parser is mandatory but only for a particular era.

This change is also a simplification of existing code. Currently FeatureValue includes optionality, which I think is better modelled via Maybe.

I think this data type will also provide us a good way to shrink our API, which would mean less code to maintain and make the handover easier.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can postpone the merge of this PR until after the next cardano-api release.

Featured
:: feature era
-- ^ The witness that the feature is supported in this era
-> a
-- ^ The value to use
-> FeatureValue feature era a

-- | No value is available for this feature in this era
NoFeatureValue
:: FeatureValue feature era a

deriving instance (Eq a, Eq (feature era)) => Eq (FeatureValue feature era a)
deriving instance (Show a, Show (feature era)) => Show (FeatureValue feature era a)
-> Featured feature era a

-- | Determine if a value is defined.
--
-- If the value is not defined, it could be because the feature is not supported or
-- because the feature is supported but the value is not available.
isFeatureValue :: FeatureValue feature era a -> Bool
isFeatureValue = \case
NoFeatureValue -> False
FeatureValue _ _ -> True
deriving instance (Eq a, Eq (feature era)) => Eq (Featured feature era a)
deriving instance (Show a, Show (feature era)) => Show (Featured feature era a)

-- | Get the value if it is defined, otherwise return the default value.
valueOrDefault :: a -> FeatureValue feature era a -> a
valueOrDefault defaultValue = \case
NoFeatureValue -> defaultValue
FeatureValue _ a -> a
instance Functor (Featured feature era) where
fmap f (Featured feature a) = Featured feature (f a)

-- | Attempt to construct a 'FeatureValue' from a value and era.
-- If the feature is not supported in the era, then 'NoFeatureValue' is returned.
asFeatureValue :: ()
asFeaturedInEra :: ()
=> FeatureInEra feature
=> a
-> CardanoEra era
-> FeatureValue feature era a
asFeatureValue value = featureInEra NoFeatureValue (`FeatureValue` value)
-> Maybe (Featured feature era a)
asFeaturedInEra value = featureInEra Nothing (Just . flip Featured value)

-- | Attempt to construct a 'FeatureValue' from a value and a shelley-based-era.
asFeatureValueInShelleyBasedEra :: ()
asFeaturedInShelleyBasedEra :: ()
=> FeatureInEra feature
=> a
-> ShelleyBasedEra era
-> FeatureValue feature era a
asFeatureValueInShelleyBasedEra value = asFeatureValue value . shelleyBasedToCardanoEra
-> Maybe (Featured feature era a)
asFeaturedInShelleyBasedEra value = asFeaturedInEra value . shelleyBasedToCardanoEra
56 changes: 56 additions & 0 deletions cardano-api/internal/Cardano/Api/Feature/ConwayEraOnwards.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

module Cardano.Api.Feature.ConwayEraOnwards
( ConwayEraOnwards(..)
, conwayEraOnwardsConstraints
, conwayEraOnwardsToCardanoEra
, conwayEraOnwardsToShelleyBasedEra
) where

import Cardano.Api.Eras

import Cardano.Crypto.Hash.Class (HashAlgorithm)
import qualified Cardano.Ledger.Api as L

data ConwayEraOnwards era where
ConwayEraOnwardsConway :: ConwayEraOnwards ConwayEra

deriving instance Show (ConwayEraOnwards era)
deriving instance Eq (ConwayEraOnwards era)

instance FeatureInEra ConwayEraOnwards where
featureInEra no yes = \case
ByronEra -> no
ShelleyEra -> no
AllegraEra -> no
MaryEra -> no
AlonzoEra -> no
BabbageEra -> no
ConwayEra -> yes ConwayEraOnwardsConway

type ConwayEraOnwardsConstraints era =
( L.EraCrypto (ShelleyLedgerEra era) ~ L.StandardCrypto
, HashAlgorithm (L.HASH (L.EraCrypto (ShelleyLedgerEra era)))
, L.ConwayEraTxBody (ShelleyLedgerEra era)
)

conwayEraOnwardsConstraints
:: ConwayEraOnwards era
-> (ConwayEraOnwardsConstraints era => a)
-> a
conwayEraOnwardsConstraints = \case
ConwayEraOnwardsConway -> id

conwayEraOnwardsToCardanoEra :: ConwayEraOnwards era -> CardanoEra era
conwayEraOnwardsToCardanoEra = shelleyBasedToCardanoEra . conwayEraOnwardsToShelleyBasedEra

conwayEraOnwardsToShelleyBasedEra :: ConwayEraOnwards era -> ShelleyBasedEra era
conwayEraOnwardsToShelleyBasedEra = \case
ConwayEraOnwardsConway -> ShelleyBasedEraConway
Loading