Swift (UI)에서`some` 키워드는 무엇입니까?


259

새로운 SwiftUI 튜토리얼 에는 다음과 같은 코드가 있습니다.

struct ContentView: View {
    var body: some View {
        Text("Hello World")
    }
}

두 번째 줄인 단어 some는 사이트에서 키워드 인 것처럼 강조 표시됩니다.

Swift 5.1에는 some키워드 가없는 것으로 보이며 단어 some가 일반적으로 수행되는 위치로 이동하기 때문에 단어 가 다른 작업을 수행 할 수없는 것으로 보입니다 . 발표되지 않은 새로운 Swift 버전이 있습니까? 내가 알지 못하는 방식으로 유형에 사용되는 함수입니까?

키워드 some는 무엇을합니까?


주제에 현기증이있는 사람들을 위해 Vadim Bulavin 덕분에 매우 해독적이고 단계별 기사가 있습니다. vadimbulavin.com/…
Luc-Olivier

답변:


333

some View이다 불투명 결과 유형 에 의해 도입으로 SE-0244 과 "반대"일반적인 자리 것으로 생각할 수 있습니다 엑스 코드 11. 스위프트 5.1에서 사용할 수는.

발신자가 만족하는 일반 일반 자리 표시 자와 달리 :

protocol P {}
struct S1 : P {}
struct S2 : P {}

func foo<T : P>(_ x: T) {}
foo(S1()) // Caller chooses T == S1.
foo(S2()) // Caller chooses T == S2.

불투명 한 결과 유형은 구현 에서 충족되는 암시 적 일반 자리 표시 자 이므로 다음과 같이 생각할 수 있습니다.

func bar() -> some P {
  return S1() // Implementation chooses S1 for the opaque result.
}

다음과 같이 보입니다 :

func bar() -> <Output : P> Output {
  return S1() // Implementation chooses Output == S1.
}

실제로이 기능의 최종 목표는보다 명확한 형식으로 리버스 제네릭을 허용하는 것 -> <T : Collection> T where T.Element == Int입니다. 예를 들어 제약 조건을 추가 할 수도 있습니다 . 자세한 내용은이 게시물을 참조하십시오 .

이것을 제거하는 가장 중요한 것은 함수 리턴 some P은 에 맞는 특정 단일 콘크리트 유형 의 값을 리턴하는 것 P입니다. 함수 내에서 다른 적합한 유형을 반환하려고하면 컴파일러 오류가 발생합니다.

// error: Function declares an opaque return type, but the return
// statements in its body do not have matching underlying types.
func bar(_ x: Int) -> some P {
  if x > 10 {
    return S1()
  } else {
    return S2()
  }
}

암시 적 일반 자리 표시자는 여러 유형으로 충족 될 수 없습니다.

이것은 반환하는 함수 대조적이다 P나타내는 데 사용될 수 있고, 둘 다 S1S2그것의 임의 나타내므로 P따르는 값 :

func baz(_ x: Int) -> P {
  if x > 10 {
    return S1()
  } else {
    return S2()
  }
}

그렇다면 불투명 한 결과 유형 -> some P은 프로토콜 반환 유형보다 어떤 이점이 -> P있습니까?


1. PAT와 함께 불투명 한 결과 유형을 사용할 수 있습니다

프로토콜의 현재 주요 제한 사항은 PAT (관련 유형의 프로토콜)를 실제 유형으로 사용할 수 없다는 것입니다. 이는 향후 버전의 언어에서 해제 될 수있는 제한 사항이지만 불투명 한 결과 유형은 사실상 일반적인 자리 표시 자이므로 오늘날 PAT와 함께 사용할 수 있습니다.

즉, 다음과 같은 작업을 수행 할 수 있습니다.

func giveMeACollection() -> some Collection {
  return [1, 2, 3]
}

let collection = giveMeACollection()
print(collection.count) // 3

2. 불투명 한 결과 유형은 동일성

불투명 한 결과 유형으로 인해 단일 콘크리트 유형이 반환되므로 컴파일러는 동일한 함수에 대한 두 번의 호출이 동일한 유형의 두 값을 반환해야한다는 것을 알고 있습니다.

즉, 다음과 같은 작업을 수행 할 수 있습니다.

//   foo() -> <Output : Equatable> Output {
func foo() -> some Equatable { 
  return 5 // The opaque result type is inferred to be Int.
}

let x = foo()
let y = foo()
print(x == y) // Legal both x and y have the return type of foo.

컴파일러가이를 알고 x있고 y구체적인 유형이 같기 때문에 이것은 합법적 입니다. 이것은 ==두 가지 유형의 매개 변수 가있는 중요한 요구 사항입니다 Self.

protocol Equatable {
  static func == (lhs: Self, rhs: Self) -> Bool
}

즉, 콘크리트 규격 유형과 동일한 유형의 두 값이 필요합니다. Equatable유형으로 사용 가능 하더라도 두 개의 임의의 Equatable일치 값을 서로 비교할 수 없습니다 .

func foo(_ x: Int) -> Equatable { // Assume this is legal.
  if x > 10 {
    return 0
  } else {
    return "hello world"      
  }
}

let x = foo(20)
let y = foo(5)
print(x == y) // Illegal.

컴파일러는 두 개의 임의 Equatable값이 동일한 기본 콘크리트 유형을 가지고 있음을 증명할 수 없습니다 .

비슷한 방식으로, 또 다른 불투명 타입 반환 함수를 도입했다면 :

//   foo() -> <Output1 : Equatable> Output1 {
func foo() -> some Equatable { 
  return 5 // The opaque result type is inferred to be Int.
}

//   bar() -> <Output2 : Equatable> Output2 {
func bar() -> some Equatable { 
  return "" // The opaque result type is inferred to be String.
}

let x = foo()
let y = bar()
print(x == y) // Illegal, the return type of foo != return type of bar.

모두 있지만 있기 때문에 예를 들어 불법이됩니다 foobar반환 some Equatable, 자신의 일반적인 자리를 "반대" Output1Output2다른 유형에 의해 만족 될 수있다.


3. 불투명 한 결과 유형은 일반 자리 표시 자로 구성됩니다.

일반 프로토콜 유형 값과 달리 불투명 한 결과 유형은 일반 일반 자리 표시 자와 함께 잘 구성됩니다.

protocol P {
  var i: Int { get }
}
struct S : P {
  var i: Int
}

func makeP() -> some P { // Opaque result type inferred to be S.
  return S(i: .random(in: 0 ..< 10))
}

func bar<T : P>(_ x: T, _ y: T) -> T {
  return x.i < y.i ? x : y
}

let p1 = makeP()
let p2 = makeP()
print(bar(p1, p2)) // Legal, T is inferred to be the return type of makeP.

예를 들어, 두 값이 서로 다른 기본 콘크리트 유형을 가질 수 있기 makeP때문에 방금 반환 된 경우 작동하지 않습니다 .PP

struct T : P {
  var i: Int
}

func makeP() -> P {
  if .random() { // 50:50 chance of picking each branch.
    return S(i: 0)
  } else {
    return T(i: 1)
  }
}

let p1 = makeP()
let p2 = makeP()
print(bar(p1, p2)) // Illegal.

콘크리트 유형에 불투명 한 결과 유형을 사용하는 이유는 무엇입니까?

이 시점에서 자신에게 생각할 수도 있습니다. 코드를 다음과 같이 작성하십시오.

func makeP() -> S {
  return S(i: 0)
}

음, 불투명 한 결과 유형을 사용하면에서 S제공하는 인터페이스 만 노출 하여 유형 을 구현 세부 사항으로 만들 수 P있으므로 함수에 의존하는 코드를 중단하지 않고 콘크리트 유형을 나중에 줄 아래로 유연하게 변경할 수 있습니다.

예를 들어 다음을 교체 할 수 있습니다.

func makeP() -> some P {
  return S(i: 0)
}

와:

func makeP() -> some P { 
  return T(i: 1)
}

호출하는 코드를 깨지 않고 makeP().

참조 불투명 유형 섹션 언어 가이드 및 신속한 진화 제안 이 기능에 대한 자세한 정보를.


20
관련이 없음 : Swift 5.1부터 return단일 표현 함수에는 필요하지 않습니다.
ielyamani

3
하지만 차이점은 다음 func makeP() -> some Pfunc makeP() -> P? 제안서를 읽었으며 샘플에 대해서도이 차이를 볼 수 없습니다.
Artem


2
스위프트 타입 처리는 엉망입니다. 이 특수성이 실제로 컴파일 타임에 처리 할 수없는 것입니까? 간단한 구문을 통해 암시 적으로 이러한 모든 경우를 처리하는 참조를 위해 C #을 참조하십시오. 스위프트는 무의식적으로 명백한 카고 컬 티스트 구문이 언어를 실제로 모호하게 만듭니다. 이에 대한 설계 이론적 근거를 설명해 주시겠습니까? (github의 제안에 대한 링크가있는 경우에도 좋습니다.) 편집 : 상단에 링크 된 것으로 나타났습니다.
SacredGeometry

2
@Zmaster 컴파일러는 두 가지 구현이 동일한 구체적 유형을 리턴하더라도 두 개의 불투명 한 리턴 유형을 다른 것으로 취급합니다. 즉, 선택한 특정 콘크리트 유형이 호출자에게 숨겨집니다. (나는이 대답을 좀 더 명시 적으로 만들기 위해 내 대답의 후반부를 확장하려고했지만 의미가 없습니다.)
Hamish 2016 년

52

다른 답변은 새 some키워드 의 기술적 측면을 잘 설명 하지만이 답변은 이유 를 쉽게 설명하려고합니다 .


프로토콜 동물이 있고 두 동물이 형제인지 비교하고 싶습니다.

protocol Animal {
    func isSibling(_ animal: Self) -> Bool
}

이런 식으로 두 동물이 같은 유형 의 동물 인 경우 두 동물이 형제인지 비교하는 것이 합리적 입니다.


이제 참조 용으로 동물의 예를 만들어 보겠습니다.

class Dog: Animal {
    func isSibling(_ animal: Dog) -> Bool {
        return true // doesn't really matter implementation of this
    }
}

없는 길 some T

이제 '가족'에서 동물을 반환하는 함수가 있다고 가정 해 봅시다.

func animalFromAnimalFamily() -> Animal {
    return myDog // myDog is just some random variable of type `Dog`
}

참고 :이 함수는 실제로 컴파일되지 않습니다. 프로토콜이 'Self'또는 generics를 사용하는 경우 '일부'기능이 추가되기 전에 프로토콜 유형을 반환 할 수 없기 때문 입니다. 그러나 당신이 할 수 있다고 가정 해 봅시다 ... 이것은 myDog를 추상 유형의 동물로 업 캐스트하는 것입니다.

이제 문제를 해결하려고하면 문제가 발생합니다.

let animal1: Animal = animalFromAnimalFamily()
let animal2: Animal = animalFromAnimalFamily()

animal1.isSibling(animal2) // error

오류가 발생 합니다.

왜? 그 이유는 animal1.isSibling(animal2)Swift에 전화를 걸 때 동물이 개인 지 고양이인지 알 수 없기 때문입니다. 스위프트는 알고있다, 지금까지대로 animal1animal2관련이없는 동물 종 수 있었다 . 우리는 다른 유형의 동물을 비교할 수 없기 때문에 (위 참조). 이 오류가 발생합니다

some T이 문제를 해결 하는 방법

이전 함수를 다시 작성해 봅시다 :

func animalFromAnimalFamily() -> some Animal {
    return myDog
}
let animal1 = animalFromAnimalFamily()
let animal2 = animalFromAnimalFamily()

animal1.isSibling(animal2)

animal1하고 animal2있습니다 하지 Animal , 하지만 그들은 클래스가 구현하는 동물이다 .

이것이 당신이 지금 할 수 있습니다 당신이 호출 할 때입니다 animal1.isSibling(animal2), 스위프트가 알고 animal1animal2같은 유형입니다.

그래서 내가 생각하는 방식 :

some T스위프트 의 어떤 구현 알고 T사용됩니다하지만 클래스의 사용자가하지 않습니다.

(자기 홍보 면책 조항) 이 새로운 기능에 대해 좀 더 깊이 있는 블로그 게시물 을 작성했습니다 (여기서와 동일한 예).


2
따라서 호출자는 호출자가 어떤 유형인지 모르더라도 함수에 대한 두 호출이 동일한 유형을 반환한다는 사실을 이용할 수 있습니다.
matt

1
@matt 본질적으로 yup. 필드 등에 사용될 때와 같은 개념입니다. 호출자에게는 반환 유형이 항상 동일한 유형이지만 유형이 정확히 무엇인지는 보증하지 않습니다.
Downgoat

@ Downgoat 완벽한 게시물과 답변을 보내 주셔서 대단히 감사합니다. some반환 유형에서 이해했듯이 함수 본문에 대한 제약 조건으로 작동합니다. 따라서 some전체 기능 본체에서 하나의 콘크리트 유형 만 반환하면됩니다. 예를 들어 : return randomDog다른 모든 반품은와 함께 만 작동해야합니다 Dog. 모든 이점은 다음과 같은 제약 조건에서 비롯됩니다. animal1.isSibling(animal2)컴파일의 가용성 및 이점 func animalFromAnimalFamily() -> some Animal(이제 Self후드에서 정의 되므로 ). 맞습니까?
Artem

5
이 줄은 내가 필요한 전부였습니다. animal1과 animal2는 Animal이 아니지만 Animal을 구현하는 클래스입니다.
전체

29

Hamish의 답변 은 매우 훌륭하고 기술적 관점에서 질문에 답변합니다. someApple의 SwiftUI 튜토리얼 에서이 특정 위치에서 키워드 가 사용되는 이유와 따라야 하는 이유에 대해 몇 가지 생각을 추가하고 싶습니다 .

some 요구 사항이 아닙니다!

우선, 의 반환 유형을 불투명 유형으로 선언 할 필요 가 없습니다 body. 를 사용하는 대신 항상 구체적 유형을 반환 할 수 있습니다 some View.

struct ContentView: View {
    var body: Text {
        Text("Hello World")
    }
}

이것도 컴파일됩니다. View의 인터페이스 를 살펴보면 의 리턴 유형이 body연관된 유형 임을 알 수 있습니다 .

public protocol View : _View {

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    associatedtype Body : View

    /// Declares the content and behavior of this view.
    var body: Self.Body { get }
}

즉, 선택한 특정 유형으로 속성 에 주석을 달아이 유형 지정합니다 body. 유일한 요구 사항은이 유형이 View프로토콜 자체 를 구현해야한다는 것입니다.

예를 들어 구현 하는 특정 유형일 수 있습니다.View

  • Text
  • Image
  • Circle

또는 을 구현 하는 불투명 한 유형 View, 즉

  • some View

일반 뷰

스택 뷰를 또는 body같은 리턴 유형 으로 사용하려고하면 문제가 발생합니다 .VStackHStack

struct ContentView: View {
    var body: VStack {
        VStack {
            Text("Hello World")
            Image(systemName: "video.fill")
        }
    }
}

이것은 컴파일되지 않으며 오류가 발생합니다.

제네릭 형식 'VStack'에 대한 참조에는 <...>

SwiftUI의 스택 뷰 가 일반적인 유형 이기 때문입니다 ! 💡 (그리고 목록 및 다른 컨테이너보기 유형 에서도 마찬가지입니다 .)

( View프로토콜을 준수하는 한 ) 모든 유형의 뷰를 여러 개 연결할 수 있기 때문에 의미가 있습니다 . VStack위 신체 의 콘크리트 유형 은 실제로

VStack<TupleView<(Text, Image)>>

나중에 스택에 뷰를 추가하기로 결정하면 콘크리트 유형이 변경됩니다. 첫 번째 텍스트 다음에 두 번째 텍스트를 추가하면

VStack<TupleView<(Text, Text, Image)>>    

텍스트와 이미지 사이에 스페이서를 추가하는 것처럼 미묘한 변화를 주더라도 스택의 유형이 변경됩니다.

VStack<TupleView<(Text, _ModifiedContent<Spacer, _FrameLayout>, Image)>>

내가 말할 수있는 건, 이다 그 이유 애플은 항상 사용에 자신의 튜토리얼에서 권장하는 이유 some View, 모든 뷰가 같이 만족하는 가장 일반적인 불투명 타입 body의 반환 유형입니다. 매번 리턴 유형을 수동으로 변경하지 않고 사용자 정의보기의 구현 / 레이아웃을 변경할 수 있습니다.


보충:

불투명 한 결과 유형을보다 직관적으로 이해하려면 최근 읽을 가치가있는 기사를 게시했습니다.

🔗 SwiftUI 에서이 "일부"는 무엇입니까?


2
이. 감사! Hamish의 답변은 매우 완전했지만이 예제에서 왜 사용되었는지 정확하게 알려줍니다.
Chris Marshall

나는 "일부"라는 아이디어를 좋아합니다. "일부"를 사용하면 컴파일 시간에 전혀 영향을 미치는지 아는 아이디어가 있습니까?
두부 전사

@ Mischa 그래서 제네릭 뷰를 만드는 방법은 무엇입니까? 뷰를 포함하는 프로토콜로 다른 행동을 취하는가?
theMouk

27

지금까지 모든 대답이 누락 된 것은 someSwiftUI와 같은 DSL (도메인 특정 언어)이나 라이브러리 / 프레임 워크와 같은 사용자에게 유용하다는 것입니다. 사용자 (다른 프로그래머)는 자신과 다릅니다.

some형식 제약 조건 대신 형식으로 사용할 수 있도록 일반 프로토콜을 래핑 할 수있는 경우를 제외하고는 일반 앱 코드에서는 절대 사용하지 않을 것입니다 . 어떤 some일은 그것의 앞에 슈퍼 외관을 넣는 동안 컴파일러가 특정 유형의 일이 무엇인지에 대한 지식을 유지하도록하는 것입니다.

따라서 당신이 사용자의 SwiftUI에, 모든 당신이 알아야 할 그 무언가가입니다 some View모든 종류의 속임수의 뒤에서 당신이 차폐되는에 갈 수 있지만. 이 객체는 실제로 매우 특정한 유형이지만 그 내용에 대해들을 필요는 없습니다. 그러나 프로토콜과 달리 본격적인 유형입니다. 프로토콜이 표시되는 곳은 특정 본격적인 유형의 외관 일뿐입니다.

SwiftUI의 향후 버전 some View에서는 개발자가 해당 특정 객체의 기본 유형을 변경할 수 있습니다. 그러나 코드가 처음부터 기본 유형을 언급하지 않았기 때문에 코드가 손상되지 않습니다.

따라서 some사실상 프로토콜을 수퍼 클래스처럼 만듭니다. 그것은이 거의 , 아니지만 꽤 실제 개체의 유형 (예를 들어, 프로토콜의 메소드 선언은 반환 할 수 없습니다 some).

당신이 사용하려고한다면 그래서 some아무것도, 경우 가장 가능성이 될 것입니다 당신이 다른 사람에 의해 사용을위한 DSL 또는 프레임 워크 / 라이브러리를 작성하고, 당신은 기본 유형의 세부 사항을 마스크 싶었다. 이를 통해 다른 사람들이 사용하기에 더 쉬운 코드를 만들 수 있으며 코드를 손상시키지 않고 구현 세부 사항을 변경할 수 있습니다.

그러나 코드의 한 영역을 코드의 다른 영역에 묻혀있는 구현 세부 정보에서 보호하는 방법으로 자체 코드에서 사용할 수도 있습니다.


23

someSwift 5.1 의 키워드 ( swift-evolution proposal )는 프로토콜과 함께 반환 유형으로 사용됩니다.

Xcode 11 릴리스 정보 는 다음과 같습니다.

함수는 정확한 반환 유형을 지정하는 대신 어떤 프로토콜을 준수하는지 선언하여 구체적인 반환 유형을 숨길 수 있습니다.

func makeACollection() -> some Collection {
    return [1, 2, 3]
}

함수를 호출하는 코드는 프로토콜의 인터페이스를 사용할 수 있지만 기본 유형에 대한 가시성은 없습니다. ( SE- 0244, 40538331)

위의 예에서을 반환한다고 말할 필요는 없습니다 Array. 그렇게하면 그냥 따르는 일반 유형을 반환 할 수도 Collection있습니다.


발생할 수있는이 가능한 오류에 유의하십시오.

'일부'리턴 유형은 iOS 13.0.0 이상에서만 사용 가능

즉, someiOS 12 및 이전 버전 에서 사용하지 않으려면 가용성을 사용해야합니다 .

@available(iOS 13.0, *)
func makeACollection() -> some Collection {
    ...
}

1
이 집중적 인 답변과 Xcode 11 베타의 컴파일러 문제에 대해 많은 감사를 전합니다.
brainray

1
someiOS 12 및 그 이전 버전에서는 피할 수 있도록 가용성을 사용해야 합니다. 당신이하는 한, 당신은 괜찮을 것입니다. 문제는 컴파일러가이를 경고하지 않는다는 것입니다.
matt

2
Cur, 당신이 지적한 것처럼 간결한 Apple 설명은 모든 것을 설명합니다 : 함수는 정확한 반환 유형을 지정하는 대신 어떤 프로토콜을 준수하는지 선언함으로써 구체적인 반환 유형을 숨길 수 있습니다. 그리고 함수를 호출하는 코드는 프로토콜 인터페이스를 사용할 수 있습니다. 단정하고 일부.
Fattie

키워드 "some"을 사용하지 않고도이 유형 (구체적인 리턴 유형 숨기기)이 가능합니다. 메소드 서명에 "some"을 추가하는 효과는 설명하지 않습니다.
빈스 오 설리번

@ VinceO'Sullivan someSwift 5.0 또는 Swift 4.2에서는이 코드 샘플에서 키워드 를 제거 할 수 없습니다 . 오류 : " 프로토콜 '컬렉션'은 자체 또는 연관된 유형 요구 사항이 있으므로 일반 제한 조건으로 만 사용할 수 있습니다. "
Cœur

2

'일부'는 불투명 한 유형을 의미합니다. SwiftUI에서 View는 프로토콜로 선언됩니다

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View {

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    associatedtype Body : View

    /// Declares the content and behavior of this view.
    var body: Self.Body { get }
}

뷰를 Struct로 만들면 View 프로토콜을 따르고 var body가 View Protocol을 확인하는 것을 반환한다고 알려줍니다. 구체적인 유형을 정의 할 필요가없는 일반적인 프로토콜 추상화와 같습니다.


1

나는 (이 무엇인지 아주 기본적인 실제 예제를 통해이 대답하려고합니다 불투명 결과 유형 에 대한)

연관된 유형의 프로토콜과이를 구현하는 두 개의 구조체가 있다고 가정합니다.

protocol ProtocolWithAssociatedType {
    associatedtype SomeType
}

struct First: ProtocolWithAssociatedType {
    typealias SomeType = Int
}

struct Second: ProtocolWithAssociatedType {
    typealias SomeType = String
}

Swift 5.1 이전에는 ProtocolWithAssociatedType can only be used as a generic constraint오류로 인해 아래가 불법입니다 .

func create() -> ProtocolWithAssociatedType {
    return First()
}

그러나 Swift 5.1에서는 이것이 좋습니다 ( some추가).

func create() -> some ProtocolWithAssociatedType {
    return First()
}

위의 SwiftUI에서 광범위하게 사용되는 실용적인 사용법입니다 some View.

그러나 한 가지 중요한 제한 사항이 있습니다-컴파일 타임에 반환 유형을 알아야하므로 아래에서 다시 Function declares an opaque return type, but the return statements in its body do not have matching underlying types오류가 발생 하지 않습니다 .

func create() -> some ProtocolWithAssociatedType {
    if (1...2).randomElement() == 1 {
        return First()
    } else {
        return Second()
    }
}

0

떠오르는 간단한 유스 케이스는 숫자 유형에 대한 일반 함수를 작성하는 것입니다.

/// Adds one to any decimal type
func addOne<Value: FloatingPoint>(_ x: Value) -> some FloatingPoint {
    x + 1
}

// Variables will be assigned 'some FloatingPoint' type
let double = addOne(Double.pi) // 4.141592653589793
let float = addOne(Float.pi) // 4.141593

// Still get all of the required attributes/functions by the FloatingPoint protocol
double.squareRoot() // 2.035090330572526
float.squareRoot() // 2.03509

// Be careful, however, not to combine 2 'some FloatingPoint' variables
double + double // OK 
//double + float // error

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.