ความโปร่งใสอ้างอิงคืออะไร?


38

ฉันได้เห็นว่าในกระบวนทัศน์ที่จำเป็น

f (x) + f (x)

อาจไม่เหมือนกับ:

2 * f (x)

แต่ในกระบวนทัศน์การทำงานมันควรจะเหมือนกัน ฉันได้ลองใช้ทั้งสองกรณีใน Python และSchemeแต่สำหรับฉันแล้วพวกเขาดูตรงไปตรงมาเหมือนกัน

อะไรคือตัวอย่างที่สามารถชี้ให้เห็นความแตกต่างกับฟังก์ชั่นที่ให้


7
คุณสามารถและมักจะเขียนฟังก์ชั่นอ้างอิงแบบโปร่งใสในไพ ธ อน ความแตกต่างคือภาษาที่ไม่บังคับใช้
Karl Bielefeldt

5
ใน C และเหมือนกัน: f(x++)+f(x++)อาจจะไม่เหมือนกัน2*f(x++)(ใน C มันน่ารักโดยเฉพาะอย่างยิ่งเมื่อสิ่งที่ซ่อนอยู่ภายในมาโคร - ฉันทำจมูกของฉันที่คุณเดิมพัน)
gnat

ในความเข้าใจของฉันตัวอย่างของ @ gnat คือสาเหตุที่ภาษาที่เน้นการใช้งานเช่น R ใช้การอ้างอิงแบบอ้างอิงและหลีกเลี่ยงฟังก์ชันที่แก้ไขข้อโต้แย้งของพวกเขาอย่างชัดเจน ใน R อย่างน้อยที่สุดมันก็ยากที่จะทำข้อ จำกัด เหล่านี้ (อย่างน้อยในรูปแบบที่มั่นคงและพกพาได้) โดยไม่ต้องขุดเข้าไปในระบบที่ซับซ้อนของภาษาของสภาพแวดล้อมและเนมสเปซและเส้นทางการค้นหา
shadowtalker

4
@ssdecontrol: ที่จริงแล้วเมื่อคุณมีความโปร่งใสในการอ้างอิงการส่งต่อค่าและการอ้างอิงผ่านจะให้ผลลัพธ์เดียวกันเสมอดังนั้นจึงไม่สำคัญว่าจะใช้ภาษาใด ภาษาที่ใช้งานได้มีการระบุบ่อยครั้งพร้อมกับบางสิ่งที่คล้ายกับการส่งผ่านตามค่าสำหรับความชัดเจนทางความหมาย แต่การใช้งานของพวกเขามักจะใช้การอ้างอิงแบบส่งต่อสำหรับประสิทธิภาพ (หรือทั้งสองอย่าง
Jörg W Mittag

4
@gnat: โดยเฉพาะอย่างยิ่งf(x++)+f(x++)สามารถเป็นอะไรก็ได้อย่างแน่นอนเพราะมันเรียกพฤติกรรมที่ไม่ได้กำหนด แต่ที่ไม่ได้เกี่ยวข้องจริงๆที่จะโปร่งใสอ้างอิง - ซึ่งจะไม่ได้ความช่วยเหลือสำหรับการโทรนี้ก็ 'ไม่ได้กำหนด' สำหรับการทำงานที่โปร่งใส referentially ในขณะที่sin(x++)+sin(x++)มากเกินไป อาจเป็น 42, สามารถฟอร์แมตฮาร์ดไดรฟ์ของคุณอาจมีปีศาจบินออกมาจากจมูกผู้ใช้ ...
Christopher Creutzig

คำตอบ:


62

Referential transparent หรือที่อ้างอิงถึงฟังก์ชั่นบ่งชี้ว่าคุณสามารถกำหนดผลลัพธ์ของการใช้ฟังก์ชันนั้นได้โดยดูที่ค่าของอาร์กิวเมนต์ คุณสามารถเขียนฟังก์ชันโปร่งใสแบบอ้างอิงได้ในภาษาการเขียนโปรแกรมเช่น Python, Scheme, Pascal, C

ในอีกหลายภาษาคุณสามารถเขียนฟังก์ชั่นที่ไม่อ้างอิงแบบโปร่งใสได้ ตัวอย่างเช่นฟังก์ชัน Python นี้:

counter = 0

def foo(x):
  global counter

  counter += 1
  return x + counter

ไม่อ้างอิงโปร่งใสในความเป็นจริงโทร

foo(x) + foo(x)

และ

2 * foo(x)

xจะผลิตค่าที่แตกต่างกันสำหรับการโต้แย้งใด ๆ เหตุผลนี้เป็นเพราะฟังก์ชั่นใช้และแก้ไขตัวแปรทั่วโลกดังนั้นผลลัพธ์ของการเรียกแต่ละครั้งขึ้นอยู่กับสถานะการเปลี่ยนแปลงนี้และไม่เพียง แต่ในการโต้แย้งฟังก์ชั่น

Haskell เป็นภาษาที่ใช้งานได้จริงโดยแยกการประเมินนิพจน์ที่ใช้ฟังก์ชั่น pureอย่างชัดเจนและโปร่งใสซึ่งอ้างอิงจากการดำเนินการ (การประมวลผลของค่าพิเศษ) ซึ่งไม่โปร่งใสอ้างอิงเช่นการดำเนินการเดียวกันสามารถมีได้ทุกครั้ง ผลลัพธ์ที่แตกต่าง

ดังนั้นสำหรับฟังก์ชั่นใด ๆ ของ Haskell

f :: Int -> Int

และจำนวนเต็มใด ๆxมันเป็นความจริงเสมอ

2 * (f x) == (f x) + (f x)

ตัวอย่างของการกระทำคือผลลัพธ์ของฟังก์ชันไลบรารีgetLine:

getLine :: IO String

อันเป็นผลมาจากการประเมินผลการแสดงออกของฟังก์ชั่นนี้ (ที่จริงคงที่) IO Stringครั้งแรกของทุกผลิตค่าบริสุทธิ์จากประเภท ค่าของประเภทนี้คือค่าที่เหมือนกัน: คุณสามารถผ่านไปมาวางไว้ในโครงสร้างข้อมูลเขียนโดยใช้ฟังก์ชั่นพิเศษและอื่น ๆ ตัวอย่างเช่นคุณสามารถสร้างรายการของการกระทำเช่นนั้น:

[getLine, getLine] :: [IO String]

การดำเนินการเป็นสิ่งพิเศษที่คุณสามารถบอกให้รันไทม์ Haskell เพื่อดำเนินการได้โดยการเขียน:

main = <some action>

ในกรณีนี้เมื่อโปรแกรม Haskell ของคุณเริ่มทำงานรันไทม์จะดำเนินการผ่านการดำเนินการที่เชื่อมโยงmainและดำเนินการซึ่งอาจทำให้เกิดผลข้างเคียง ดังนั้นการดำเนินการแอคชั่นจึงไม่โปร่งใสเนื่องจากการดำเนินการแอ็คชันเดียวกันสองครั้งสามารถสร้างผลลัพธ์ที่แตกต่างกันขึ้นอยู่กับสิ่งที่รันไทม์ได้รับเป็นอินพุต

ต้องขอบคุณระบบประเภทของ Haskell จึงไม่สามารถใช้การกระทำในบริบทที่คาดว่าจะมีประเภทอื่นและในทางกลับกัน ดังนั้นหากคุณต้องการค้นหาความยาวของสตริงคุณสามารถใช้lengthฟังก์ชัน:

length "Hello"

จะส่งคืน 5. แต่ถ้าคุณต้องการค้นหาความยาวของสตริงที่อ่านจากเทอร์มินัลคุณไม่สามารถเขียนได้

length (getLine)

เนื่องจากคุณได้รับข้อผิดพลาดประเภท: lengthคาดว่าอินพุตของรายการประเภท (และสตริงคือแท้จริงรายการ) แต่getLineเป็นค่าประเภทIO String(การกระทำ) วิธีนี้จะช่วยให้มั่นใจระบบการพิมพ์ที่มีมูลค่าการกระทำเช่นgetLine(ที่มีการดำเนินการจะดำเนินการนอกภาษาหลักและซึ่งอาจจะไม่ใช่ referentially โปร่งใส) Intไม่สามารถที่ซ่อนอยู่ภายในค่าที่ไม่ใช่การกระทำของประเภท

แก้ไข

เพื่อตอบคำถาม exizt ต่อไปนี้เป็นโปรแกรม Haskell ขนาดเล็กที่อ่านบรรทัดจากคอนโซลและพิมพ์ความยาว

main :: IO () -- The main program is an action of type IO ()
main = do
          line <- getLine
          putStrLn (show (length line))

การดำเนินการหลักประกอบด้วยสองการโต้ตอบที่ดำเนินการตามลำดับ:

  1. getlineประเภทIO String,
  2. ที่สองคือการสร้างโดยการประเมินฟังก์ชั่นputStrLnของประเภทString -> IO ()บนการโต้แย้ง

แม่นยำยิ่งกว่าการกระทำที่สองถูกสร้างขึ้นโดย

  1. ผูกพันlineกับค่าที่อ่านโดยการกระทำแรก
  2. การประเมินฟังก์ชั่นแท้length(คำนวณความยาวเป็นจำนวนเต็ม) แล้วshow(เปลี่ยนจำนวนเต็มเป็นสตริง)
  3. การสร้างการดำเนินการโดยใช้ฟังก์ชั่นเพื่อผลมาจากการputStrLnshow

ณ จุดนี้การดำเนินการที่สองสามารถดำเนินการได้ หากคุณพิมพ์ "Hello" ระบบจะพิมพ์ "5"

โปรดทราบว่าหากคุณได้รับค่าจากการกระทำโดยใช้<-สัญกรณ์คุณสามารถใช้ค่านั้นในการดำเนินการอื่นเช่นคุณไม่สามารถเขียน:

main = do
          line <- getLine
          show (length line) -- Error:
                             -- Expected type: IO ()
                             --   Actual type: String

เพราะshow (length line)มีประเภทStringในขณะที่เครื่องหมายต้องกำหนดว่าการกระทำ ( getLineประเภทIO String) ตามด้วยการกระทำอื่น (เช่นputStrLn (show (length line))ประเภทIO ())

แก้ไข 2

คำจำกัดความความโปร่งใสในการอ้างอิงของJörg W Mittag นั้นกว้างกว่าของฉันมาก (ฉันตอบคำตอบของเขาแล้ว) ฉันได้ใช้คำจำกัดความที่ถูก จำกัด เพราะตัวอย่างในคำถามมุ่งเน้นไปที่ค่าส่งคืนของฟังก์ชั่นและฉันต้องการแสดงแง่มุมนี้ อย่างไรก็ตาม RT โดยทั่วไปหมายถึงความหมายของโปรแกรมทั้งหมดรวมถึงการเปลี่ยนแปลงสถานะโลกและการโต้ตอบกับสภาพแวดล้อม (IO) ที่เกิดจากการประเมินการแสดงออก ดังนั้นสำหรับคำจำกัดความทั่วไปที่ถูกต้องคุณควรอ้างถึงคำตอบนั้น


10
Downvoter สามารถแนะนำวิธีการปรับปรุงคำตอบนี้ได้อย่างไร
จอร์โจ

ดังนั้นหนึ่งจะได้รับความยาวของสตริงอ่านจาก terminal ใน Haskell ได้อย่างไร
sbichenko

2
นี่เป็นเรื่องที่หยาบคายอย่างมาก แต่เพื่อความสมบูรณ์แบบมันไม่ใช่ระบบแบบของ Haskell ที่รับประกันการกระทำและฟังก์ชั่นที่บริสุทธิ์ไม่ได้ผสมกัน ความจริงที่ว่าภาษาไม่ได้มีฟังก์ชั่นไม่บริสุทธิ์ใด ๆ ที่คุณสามารถโทรได้โดยตรง คุณสามารถนำIOรูปแบบของ Haskell ไปใช้ได้จริงในภาษาใด ๆ กับ lambdas และ generics แต่เนื่องจากใครก็ตามสามารถโทรออกprintlnได้โดยตรงการใช้งานIOไม่รับประกันความบริสุทธิ์ มันเป็นเพียงแค่การประชุม
Doval

ฉันหมายถึงว่า (1) ฟังก์ชั่นทั้งหมดนั้นบริสุทธิ์ (แน่นอนพวกเขาบริสุทธิ์เพราะภาษาไม่ได้ให้สิ่งที่ไม่บริสุทธิ์แม้ว่าเท่าที่ฉันรู้ว่ามีกลไกบางอย่างที่จะหลีกเลี่ยงสิ่งนั้น) และ (2) ฟังก์ชั่นบริสุทธิ์และ การกระทำที่ไม่บริสุทธิ์มีประเภทต่าง ๆ ดังนั้นจึงไม่สามารถผสมกันได้ BTW ทำในสิ่งที่คุณหมายถึงโดยโทรโดยตรง ?
จอร์โจ

6
จุดของคุณเกี่ยวกับการgetLineไม่โปร่งใส referential ไม่ถูกต้อง คุณกำลังนำเสนอgetLineราวกับว่ามันประเมินหรือลดให้กับสตริงบางอย่างสตริงเฉพาะซึ่งขึ้นอยู่กับการป้อนข้อมูลของผู้ใช้ สิ่งนี้ไม่ถูกต้อง IO Stringไม่ได้มีเชือกใด ๆ มากกว่าMaybe Stringไม่ IO Stringเป็นสูตรสำหรับบางทีอาจจะได้รับสตริงและเป็นนิพจน์มันบริสุทธิ์เหมือนคนอื่น ๆ ใน Haskell
LuxuryMode

25
def f(x): return x()

from random import random
f(random) + f(random) == 2*f(random)
# => False

อย่างไรก็ตามนั่นไม่ใช่ความโปร่งใส Referential Transparency RT หมายความว่าคุณสามารถแทนที่นิพจน์ใด ๆในโปรแกรมด้วยผลลัพธ์ของการประเมินนิพจน์นั้น (หรือกลับกัน) โดยไม่เปลี่ยนความหมายของโปรแกรม

ยกตัวอย่างเช่นโปรแกรมต่อไปนี้:

def f(): return 2

print(f() + f())
print(2)

โปรแกรมนี้มีความโปร่งใสในการอ้างอิง ฉันสามารถแทนที่หนึ่งหรือทั้งสองเหตุการณ์f()ด้วย2และจะยังคงทำงานเหมือนเดิม:

def f(): return 2

print(2 + f())
print(2)

หรือ

def f(): return 2

print(f() + 2)
print(2)

หรือ

def f(): return 2

print(2 + 2)
print(f())

จะทำตัวเหมือนเดิม

ที่จริงฉันโกง ฉันควรจะสามารถแทนที่การเรียกprintด้วยค่าที่ส่งคืน (ซึ่งไม่มีค่าเลย) โดยไม่เปลี่ยนความหมายของโปรแกรม อย่างไรก็ตามชัดเจนถ้าฉันเพิ่งลบทั้งสองprintงบความหมายของโปรแกรมจะเปลี่ยน: ก่อนมันพิมพ์บางสิ่งบางอย่างไปที่หน้าจอหลังจากที่มันไม่ได้ I / O ไม่อ้างอิงอย่างโปร่งใส

กฎง่ายๆคือ: ถ้าคุณสามารถแทนที่การแสดงออกใด ๆ การแสดงออกย่อยหรือการเรียกรูทีนย่อยด้วยค่าตอบแทนของการแสดงออกนั้นการแสดงออกย่อยหรือการเรียกรูทีนย่อยที่ใดก็ได้ในโปรแกรมโดยไม่มีโปรแกรมเปลี่ยนความหมายของมันแล้วคุณมีการอ้างอิง ความโปร่งใส และสิ่งนี้หมายความว่าการพูดในทางปฏิบัติคือคุณไม่สามารถมี I / O ใด ๆ ไม่สามารถมีสถานะที่เปลี่ยนแปลงไม่ได้ไม่มีผลข้างเคียงใด ๆ ในทุกนิพจน์ค่าของนิพจน์ต้องขึ้นอยู่กับค่าของส่วนต่าง ๆ ของนิพจน์เท่านั้น และในการเรียกรูทีนย่อยทุกครั้งค่าส่งคืนจะต้องขึ้นอยู่กับอาร์กิวเมนต์เท่านั้น


4
"ไม่สามารถมีสถานะที่ไม่แน่นอน": คุณสามารถมีได้หากถูกซ่อนอยู่และไม่มีผลต่อพฤติกรรมที่สังเกตได้ของรหัสของคุณ คิดเช่นเกี่ยวกับการบันทึก
จอร์โจ

4
@ จอร์โจ: นี่อาจเป็นอัตนัย แต่ฉันยืนยันว่าผลการแคชไม่ได้เป็น "รัฐที่ไม่แน่นอน" หากพวกเขาซ่อนและไม่มีผลที่สังเกตได้ ความไม่สามารถเปลี่ยนแปลงได้นั้นเป็นนามธรรมที่ถูกนำมาใช้บนฮาร์ดแวร์ที่เปลี่ยนแปลงได้ บ่อยครั้งมีการจัดทำโดยภาษา (ให้นามธรรมของ "ค่า" แม้ว่าค่าสามารถย้ายระหว่างการลงทะเบียนและตำแหน่งหน่วยความจำในระหว่างการดำเนินการและสามารถหายไปเมื่อมันรู้ว่ามันจะไม่ถูกใช้อีกครั้ง) แต่มันไม่ถูกต้องเมื่อมัน จัดหาโดยห้องสมุดหรืออะไรก็ตาม (สมมติว่ามันใช้งานได้อย่างถูกต้องแน่นอน)
ruakh

1
+1 ฉันชอบprintตัวอย่างมาก บางทีวิธีหนึ่งในการดูสิ่งนี้คือสิ่งที่พิมพ์บนหน้าจอเป็นส่วนหนึ่งของ "ค่าส่งคืน" หากคุณสามารถแทนที่printด้วยฟังก์ชันส่งคืนค่าและการเขียนที่เทียบเท่าบนเทอร์มินัลตัวอย่างก็ใช้ได้
Pierre Arlaud

1
@Giorgio Space / time ไม่สามารถพิจารณาถึงผลข้างเคียงเพื่อความโปร่งใสในการอ้างอิง ที่จะทำให้4และ2 + 2ไม่สามารถเปลี่ยนแปลงได้เนื่องจากพวกเขามีเวลาทำงานที่แตกต่างกันและจุดรวมของความโปร่งใสอ้างอิงคือคุณสามารถทดแทนการแสดงออกด้วยสิ่งที่มันประเมิน การพิจารณาที่สำคัญคือความปลอดภัยของเธรด
Doval

1
@overexchange: Referential Transparency หมายถึงคุณสามารถแทนที่ subexpression ทุกอันด้วยค่าของมันโดยไม่ต้องเปลี่ยนความหมายของโปรแกรม listOfSequence.append(n)ส่งคืนNoneดังนั้นคุณควรจะสามารถแทนที่การโทรทุกครั้งlistOfSequence.append(n)ด้วยNoneโดยไม่ต้องเปลี่ยนความหมายของโปรแกรมของคุณ คุณสามารถทำได้ไหม? ถ้าไม่เช่นนั้นมันจะไม่โปร่งใสอ้างอิง
Jörg W Mittag

1

บางส่วนของคำตอบนี้นำมาโดยตรงจากบทช่วยสอนที่ยังไม่เสร็จเกี่ยวกับการเขียนโปรแกรมใช้งานได้โฮสต์บนบัญชี GitHub ของฉัน:

ฟังก์ชั่นได้รับการกล่าวถึงว่ามีความโปร่งใสในการอ้างอิงหากได้รับพารามิเตอร์อินพุตเดียวกันจะสร้างเอาต์พุตเดียวกันเสมอ (ค่าส่งคืน) หากใครกำลังมองหา raison d'êtreสำหรับการเขียนโปรแกรมการทำงานที่บริสุทธิ์ความโปร่งใสในการอ้างอิงเป็นตัวเลือกที่ดี เมื่อให้เหตุผลกับสูตรในพีชคณิตคณิตศาสตร์และตรรกะคุณสมบัตินี้ - เรียกอีกอย่างว่าการแทนของเท่ากับเท่ากับ - เป็นสิ่งสำคัญพื้นฐานที่มักจะได้รับ ...

ลองพิจารณาตัวอย่างง่ายๆ:

x = 42

ในภาษาที่ใช้งานได้จริงด้านซ้ายและด้านขวาของเครื่องหมายเท่ากับจะถูกแทนที่ซึ่งกันและกันทั้งสองวิธี นั่นคือซึ่งแตกต่างจากในภาษาเช่น C, สัญกรณ์ดังกล่าวข้างต้นยืนยันความเท่าเทียมกันอย่างแท้จริง ผลที่ตามมาคือเราสามารถให้เหตุผลเกี่ยวกับรหัสโปรแกรมเช่นเดียวกับสมการทางคณิตศาสตร์

จากHaskell wiki :

การคำนวณแบบบริสุทธิ์ให้ค่าเดียวกันทุกครั้งที่เรียกใช้ คุณสมบัตินี้เรียกว่าความโปร่งใสในการอ้างอิงและทำให้เป็นไปได้ในการดำเนินการให้เหตุผลที่เท่าเทียมกันในรหัส ...

เพื่อความคมชัดนี้ประเภทของการดำเนินงานที่ดำเนินการโดย C-เช่นภาษาบางครั้งจะเรียกว่าได้รับมอบหมายทำลาย

คำที่บริสุทธิ์มักใช้เพื่ออธิบายคุณสมบัติของนิพจน์ที่เกี่ยวข้องกับการสนทนานี้ สำหรับฟังก์ชั่นที่จะถือว่าบริสุทธิ์

  • ไม่อนุญาตให้แสดงผลข้างเคียงใด ๆ และ
  • มันจะต้องโปร่งใส referential

ตามคำอุปมาดำที่พบในตำราคณิตศาสตร์จำนวนมาก internals ของฟังก์ชั่นจะถูกปิดผนึกอย่างสมบูรณ์จากโลกภายนอก ผลข้างเคียงคือเมื่อฟังก์ชั่นหรือการแสดงออกละเมิดหลักการนี้ - นั่นคือขั้นตอนได้รับอนุญาตให้สื่อสารในทางใดทางหนึ่งกับหน่วยโปรแกรมอื่น ๆ (เช่นเพื่อแบ่งปันและแลกเปลี่ยนข้อมูล)

โดยสรุปความโปร่งใสในการอ้างอิงเป็นสิ่งจำเป็นสำหรับฟังก์ชั่นที่จะทำตัวเหมือนจริงฟังก์ชั่นทางคณิตศาสตร์ยังอยู่ในความหมายของภาษาการเขียนโปรแกรม


นี้น่าจะเปิดขึ้นพร้อมกับคำโดยคำคัดลอกเอามาจากที่นี่ : "ฟังก์ชั่นบอกว่าจะมีความโปร่งใส referentially ถ้ามันได้รับการป้อนพารามิเตอร์เดียวกันเสมอผลิตผลผลิตเดียวกัน ..." กอง Exchange มีกฎสำหรับการขโมยความคิดเป็น คุณรู้เกี่ยวกับสิ่งเหล่านี้หรือไม่ "การขโมยความคิดเป็นการกระทำที่ไร้สาระในการลอกชิ้นส่วนของงานของคนอื่นตบชื่อของคุณและส่งต่อตัวคุณเองในฐานะผู้เขียนต้นฉบับ ... "
gnat

3
ฉันเขียนหน้านั้น
yesthisisuser

หากเป็นกรณีนี้ให้ลองทำให้การลอกเลียนแบบดูน้อยลงเพราะผู้อ่านไม่มีทางที่จะบอกได้ คุณรู้วิธีการทำเช่นนี้ที่ SE หรือไม่? 1) คุณอ้างอิงต้นฉบับต้นฉบับเช่น "As (ฉันมี) [here](link to source)... " ตามด้วย 2) การจัดรูปแบบเครื่องหมายคำพูดที่เหมาะสม (ใช้เครื่องหมายคำพูดหรือ> สัญลักษณ์ที่ดีกว่านั้น) มันจะไม่เจ็บถ้านอกเหนือจากการให้คำแนะนำทั่วไปคำตอบที่อยู่คำถามที่เป็นรูปธรรมถามเกี่ยวกับในกรณีนี้เกี่ยวกับf(x)+f(x)/ 2*f(x)ดูวิธีการตอบ - มิฉะนั้นอาจดูเหมือนว่าคุณเพียงแค่โฆษณาหน้าของคุณ
ริ้น

1
ในทางทฤษฎีฉันเข้าใจคำตอบนี้ แต่ในทางปฏิบัติตามกฎเหล่านี้ฉันต้องส่งคืนรายการลำดับ hailstone ในโปรแกรมนี้ ฉันจะทำสิ่งนี้ได้อย่างไร
แลกเปลี่ยน
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.