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

core and sdk: Add Hash and Show instances #331

Merged
merged 5 commits into from
Nov 1, 2023

Conversation

iRevive
Copy link
Contributor

@iRevive iRevive commented Oct 3, 2023

Motivation

  • Provide a universal typesafe option to compare two instances
  • Meaningful .toString for debugging purposes (the format is identical to open-telemetry java)
  • To preserve binary compatibility, most data types in Experimental sdk-trace module #325 are represented as sealed traits. Hash and Show are used to calculate hash and toString for such data types
  • Out of the box integration with typelevel ecosystem (e.g. weaver-test)

@iRevive iRevive changed the title core-common and sdk-common: Add Hash and Show instances core and sdk: Add Hash and Show instances Oct 3, 2023
Comment on lines 83 to 84
implicit val attributeKeyAnyHash: Hash[AttributeKey[_]] =
Hash.by(key => (key.name, key.`type`))
Copy link
Member

Choose a reason for hiding this comment

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

This is a bit weird, we definitely don't want to keep it gated in a testkit or behind an extra import?

Either way, Any is distinct from _ which I think is called "existential".

Copy link
Contributor Author

@iRevive iRevive Oct 3, 2023

Choose a reason for hiding this comment

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

I guess we can keep it behind the certain import.

There are several places in (upcoming) Otel4s SDK where we don't know a concrete type of an Attritube

Copy link
Contributor Author

@iRevive iRevive Oct 3, 2023

Choose a reason for hiding this comment

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

Could you suggest the object's name where I can place this implicit, please?

Should the structure be similar to something like this?:

object AttributeKey {
  object Implicits {
    object Extra {
      implicit val attributeKeyExistentialHash: Hash[AttributeKey[_]] = ...
    }
  }
}

Extra / Existential / Unsafe / Internal?

Copy link
Contributor Author

@iRevive iRevive Oct 4, 2023

Choose a reason for hiding this comment

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

Hm, we already define some Show[Attribute[_]] instances in the public implicit lookup scope:

implicit val showAttribute: Show[Attribute[_]] = (a: Attribute[_]) =>
.

Copy link
Contributor Author

@iRevive iRevive Oct 11, 2023

Choose a reason for hiding this comment

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

@armanbilge what are the implications if we keep existential instances in the public scope?

We need existential instances for:

  1. AttributeType[_] - a sealed trait with case objects, universal hashcode is fine here
  2. AttributeKey[_] - created from key => (key.name, key.type), where the name is a String and the type is an AttributeType[_] (a coproduct)
  3. Attribute[_] - created from a => (a.key, a.value), where the key is AttributeKey, the value is one of Boolean, Double, String, Long, List[Boolean], List[Double], List[String], List[Long].

The Hash[Attribute[List[String]]] should generate the same hashcode as Hash[Attribute[_]].

Copy link
Member

Choose a reason for hiding this comment

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

If we are confident that universal hashing will work for all the possible types (now and future) then it seems fine. e.g. could there ever be an attribute for a CIString?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Spec says the following:

The attribute value is either:

  • A primitive type: string, boolean, double precision floating point (IEEE 754-1985) or signed 64 bit integer.
  • An array of primitive type values. The array MUST be homogeneous, i.e., it MUST NOT contain values of different types.

I don't think we should allow any other types other than these.
So, I doubt Attribute[CIString] should be a thing.

Copy link
Contributor

Choose a reason for hiding this comment

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

regardless, CIString has a case-insensitive hashCode implementation, so universal hashing should still work anyway

Copy link
Member

Choose a reason for hiding this comment

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

regardless, CIString has a case-insensitive hashCode implementation

Yes, it does today, but I'm considering a future where we might want to make it an opaque type.

@@ -76,4 +76,8 @@ String, Boolean, Long, Double, List[String], List[Boolean], List[Long], List[Dou
implicit def hashAttribute[T: Hash]: Hash[Attribute[T]] =
Hash.by(a => (a.key, a.value))

implicit val hashAttributeExistential: Hash[Attribute[_]] = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently, the attribute value can exactly be one of: Boolean, Double, String, Long, List[Boolean], List[Double], List[String], List[Long].

So using a universal hashcode should be safe here.

Comment on lines +108 to +129
implicit val attributeExistentialCogen: Cogen[Attribute[_]] =
Cogen { (seed, attr) =>
def primitive[A: Cogen](seed: Seed): Seed =
Cogen[A].perturb(seed, attr.value.asInstanceOf[A])

def list[A: Cogen](seed: Seed): Seed =
Cogen[List[A]].perturb(seed, attr.value.asInstanceOf[List[A]])

val valueCogen: Seed => Seed = attr.key.`type` match {
case AttributeType.Boolean => primitive[Boolean]
case AttributeType.Double => primitive[Double]
case AttributeType.String => primitive[String]
case AttributeType.Long => primitive[Long]
case AttributeType.BooleanList => list[Boolean]
case AttributeType.DoubleList => list[Double]
case AttributeType.StringList => list[String]
case AttributeType.LongList => list[Long]
}

valueCogen(attributeKeyCogen.perturb(seed, attr.key))
}

Copy link
Contributor Author

@iRevive iRevive Oct 4, 2023

Choose a reason for hiding this comment

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

@armanbilge I didn't use Cogen before. Am I defining it correctly?

Do I understand this in the right way:

  1. Gen - produces a value of type A from Seed
  2. Cogen - produces a Seed from the given value A

Is Cogen used to adjust the seed for the structured values, and, as a result, produce better pseudo-random values?

Copy link
Member

@armanbilge armanbilge Oct 4, 2023

Choose a reason for hiding this comment

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

Is Cogen used to adjust the seed for the structured values, and, as a result, produce better pseudo-random values?

Cogen is used to generate arbitrary lambdas e.g. Arbitrary[A => B]. If you have Cogen[A] and Gen[B] then you can chain A => Seed, Seed => Seed, and Seed => B to get an arbitrary but deterministic A => B function.


I think your implementation looks good, typically I try to avoid the manual plumbing of seeds and instead transform to some datatype for which Cogen already exists. In this case that would be very messy 😁 but it could look something like this (basically an Either encoding of a sum type).

Either[
  Boolean,
  Either[
    Double,
    Either[
      String,
      Either[
        Long,
        Either[...]
      ]
    ]
  ]
]

@iRevive iRevive added this to the 0.4 milestone Oct 11, 2023
@iRevive iRevive requested a review from armanbilge October 13, 2023 08:12
@iRevive iRevive modified the milestones: 0.4, 0.3 Oct 28, 2023
@iRevive iRevive merged commit c2c9e97 into typelevel:main Nov 1, 2023
9 checks passed
@iRevive iRevive deleted the hash-show-instances branch November 1, 2023 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants