0Pricing
Scala for Backend Engineering & Functional Programming · Lekcja

Newtype

Bezpieczne typowo opakowania

Newtype to bezpłatna lekcja Scala for Backend Engineering & Functional Programming na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Scala for Backend Engineering & Functional Programming, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Scala for Backend Engineering & Functional Programming zawiera 4 lekcji w sumie.

Bezpieczne typowo opakowania

Newtype to odrębny typ opakowujący pojedynczą wartość bazową. Zapobiega mieszaniu wartości o tej samej reprezentacji prymitywnej, takich jak UserId i ProductId, które są typu Int.

case class UserId(value: Int)
case class ProductId(value: Int)

object Main:
  def main(args: Array[String]): Unit =
    val u = UserId(1)
    val p = ProductId(1)
    println(u.value == p.value) // values equal, types differ

Problem obsesji na punkcie prymitywów

Obsesja na punkcie prymitywów polega na nadmiernym używaniu String i Int do reprezentowania pojęć domenowych. Pozwala to na bezsensowne sytuacje, takie jak przekazanie adresu e-mail w miejscu, w którym oczekiwane jest imię. Newtypes rozwiązują ten problem, nadając każdemu pojęciu własny typ.

case class Email(value: String)
case class City(value: String)

object Main:
  def greet(c: City): String = s"Welcome to ${c.value}"

  def main(args: Array[String]): Unit =
    println(greet(City("Berlin")))

Newtypes w case class

Najprostszym newtype jest jednopólowa case class. Zapewnia ona równość, czytelną metodę toString i dopasowywanie wzorców od razu po utworzeniu. Kosztem jest utworzenie jednego obiektu dla każdej wartości.

case class OrderId(value: String)

object Main:
  def main(args: Array[String]): Unit =
    val id = OrderId("ORD-42")
    println(id)
    println(id.value)

Newtypes jako typy niejawne

Aby uniknąć tworzenia obiektów, zaimplementuj newtype za pomocą typu niejawnego. W czasie działania zachowuje się jak typ bazowy, ale w czasie kompilacji jest odrębnym typem.

object Ids:
  opaque type UserId = Int
  def apply(i: Int): UserId = i
  extension (u: UserId) def value: Int = u

object Main:
  def main(args: Array[String]): Unit =
    val u = Ids(7)
    println(u.value)

Dodawanie zachowania za pomocą rozszerzeń

Udostępniaj operacje dla newtype za pomocą metod extension, wystawiając tylko te, które mają sens dla danego pojęcia domenowego.

object Money:
  opaque type Cents = Long
  def apply(n: Long): Cents = n
  extension (c: Cents)
    def +(o: Cents): Cents = c + o
    def toDollars: Double = c / 100.0

object Main:
  def main(args: Array[String]): Unit =
    val total = Money(250) + Money(750)
    println(total.toDollars)

Newtypes w sygnaturach funkcji

Newtypes sprawiają, że sygnatury same dokumentują swoje znaczenie, i wykrywają błędy kolejności argumentów w czasie kompilacji. Nie można przypadkowo zamienić dwóch parametrów o różnych newtypes.

case class Width(value: Int)
case class Height(value: Int)

object Main:
  def area(w: Width, h: Height): Int = w.value * h.value

  def main(args: Array[String]): Unit =
    println(area(Width(4), Height(5)))

Newtypes i kolekcje

Map, którego kluczem jest newtype, jest czytelniejszy i bezpieczniejszy niż mapa z kluczem typu raw Int. Typ uniemożliwia użycie niewłaściwego rodzaju klucza.

case class UserId(value: Int)

object Main:
  def main(args: Array[String]): Unit =
    val names = Map(UserId(1) -> "Ada", UserId(2) -> "Bob")
    println(names(UserId(2)))

Zwalidowane newtypes

Połącz newtype z inteligentnym konstruktorem, aby opakowanie również wymuszało niezmienniki. NonEmptyString nie może być wtedy pusty.

case class NonEmptyString private (value: String)
object NonEmptyString:
  def of(s: String): Option[NonEmptyString] =
    if s.nonEmpty then Some(NonEmptyString(s)) else None

object Main:
  def main(args: Array[String]): Unit =
    println(NonEmptyString.of("hi"))
    println(NonEmptyString.of(""))

Wybór reprezentacji

Wybierz rozwiązanie zależnie od potrzeb.

  • case class: najprostsza, obsługuje dopasowywanie wzorców i tworzy obiekt.
  • opaque type: brak kosztu, brak boxingu, idealny dla często wykonywanych ścieżek i dużych kolekcji.
object Temp:
  opaque type Kelvin = Double
  def apply(d: Double): Kelvin = d
  extension (k: Kelvin) def value: Double = k

object Main:
  def main(args: Array[String]): Unit =
    val readings = List(Temp(300.0), Temp(310.5))
    println(readings.map(_.value).sum)

Zapobieganie przypadkowym konwersjom

Dwa newtypes oparte na tym samym typie bazowym nie konwertują się niejawnie. Aby przejść między nimi, należy napisać jawną funkcję, dokumentującą intencję.

case class Meters(value: Double)
case class Feet(value: Double)

object Main:
  def toFeet(m: Meters): Feet = Feet(m.value * 3.281)

  def main(args: Array[String]): Unit =
    println(toFeet(Meters(2.0)))

Kiedy używać newtypes

Sięgaj po newtypes zawsze, gdy prymityw niesie znaczenie domenowe.

  • Identyfikatory, jednostki, ciągi znaków w określonym formacie.
  • Używaj case class dla prostoty, a opaque type dla wydajności.
  • Dodaj inteligentny konstruktor, gdy występują niezmienniki.
object Domain:
  opaque type Sku = String
  def of(s: String): Option[Sku] =
    if s.startsWith("SKU-") then Some(s) else None
  extension (k: Sku) def raw: String = k

object Main:
  def main(args: Array[String]): Unit =
    println(Domain.of("SKU-9").map(_.raw))

Szybkie sprawdzenie

Sprawdź swoją wiedzę na temat newtypes.

Podsumowanie

Poznał(a) Pan/Pani newtypes.

  • Newtypes opakowują jedną wartość bazową w odrębny typ.
  • Rozwiązują problem obsesji na punkcie prymitywów i zapobiegają pomyłkom argumentów.
  • Używaj case class dla prostoty, a opaque type dla braku kosztu.
  • Dodaj inteligentny konstruktor do zwalidowanych opakowań.
  • Konwersje między newtypes są zawsze jawne.
object Ids:
  opaque type AccountId = Long
  def apply(n: Long): AccountId = n
  extension (a: AccountId) def value: Long = a

object Main:
  def main(args: Array[String]): Unit =
    val acc = Ids(1001L)
    println(acc.value)

Często zadawane pytania

Czy lekcja „Newtype” jest bezpłatna?

Tak — pełny tekst „Newtype” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Scala for Backend Engineering & Functional Programming, przejdź na CoddyKit PRO. Kurs Scala for Backend Engineering & Functional Programming zawiera 4 lekcji w sumie.

Co nauczysz się w „Newtype”?

Bezpieczne typowo opakowania Ćwiczysz Scala for Backend Engineering & Functional Programming z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Scala for Backend Engineering & Functional Programming?

Nie wymagamy żadnego doświadczenia. Scala for Backend Engineering & Functional Programming w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Newtype”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Scala for Backend Engineering & Functional Programming?

Tak. Każda lekcja Scala for Backend Engineering & Functional Programming zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Modelowanie za pomocą ADT
  2. Inteligentne konstruktory
  3. Newtype
  4. Komponowanie domen
← Powrót do Scala for Backend Engineering & Functional Programming