Tür denetleyici çok yanlış tür değiştirmeye izin veriyor ve program hala


100

Programımda bir sorunu gidermeye çalışırken (eşit yarıçaplı 2 daire Gloss kullanılarak farklı boyutlara çekiliyor *), tuhaf bir durumla karşılaştım. Nesneleri işleyen dosyamda, a için aşağıdaki tanıma sahibim Player:

type Coord = (Float,Float)
data Obj =  Player  { oPos :: Coord, oDims :: Coord }

ve Objects.hs dosyasını içe aktaran ana dosyamda aşağıdaki tanıma sahibim:

startPlayer :: Obj
startPlayer = Player (0,0) 10

Bu, oyuncu için alanlar eklemem ve değiştirmem ve daha startPlayersonra güncellemeyi unutmam nedeniyle oldu (boyutları bir yarıçapı temsil etmek için tek bir sayı ile belirlendi, ancak bunu Coordtemsil etmek için (genişlik, yükseklik) olarak değiştirdim; oyuncu daire olmayan bir nesneye itiraz eder).

Şaşırtıcı olan şey, ikinci alanın yanlış türde olmasına rağmen yukarıdaki kod derlenir ve çalışır.

İlk önce dosyaların farklı sürümlerinin açık olabileceğini düşündüm, ancak herhangi bir dosyadaki değişiklikler derlenen programa yansıdı.

Sonra bunun bir startPlayernedenden dolayı kullanılmadığını düşündüm . Üzerinden yorumlayan startPlayerbir derleyici hatası da, hatta yabancı verir, değiştirilmesi 10de startPlayernedenleri, uygun bir tepki (başlangıç boyutu değişir Player); yine, yanlış türde olmasına rağmen. Veri tanımını doğru okuduğundan emin olmak için dosyaya bir yazım hatası ekledim ve bu bana bir hata verdi; bu yüzden doğru dosyaya bakıyorum.

Ben kendi dosyasına yukarıdaki 2 parçacıklarını yapıştırarak denedim ve ikinci alanda bu beklenen hatayı tükürdü Playeriçinde startPlayeryanlıştır.

Bunun olmasına ne izin verebilir? Haskell'in tür denetleyicisinin engellemesi gereken şeyin bu olduğunu düşünürsünüz.


* İlk sorunumun cevabı, sözde eşit yarıçaplı iki dairenin farklı boyutlara çekilmesi, yarıçaplardan birinin aslında negatif olmasıydı.


26
@Cubic'in belirttiği gibi, bu sorunu kesinlikle Gloss geliştiricilerine bildirmelisiniz. Sorunuz güzel bir kütüphanenin uygunsuz yetim örneği berbat nasıl göstermektedir senin kodu.
Christian Conkle

1
Bitti. Örnekleri hariç tutmak mümkün mü? Kütüphanenin çalışması için buna ihtiyaç duyabilirler ama buna ihtiyacım yok. Num Color'ı da tanımladıklarını fark ettim. Bunun beni tuzağa düşürmesi an meselesi.
Carcigenicate

@Cubic Pekala, çok geç. Ve bunu yalnızca bir hafta kadar önce güncellenmiş, güncel bir Cabal kullanarak indirdim; bu yüzden güncel olmalıdır.
Carcigenicate

2
@ChristianConkle Gloss yazarının TypeSynonymInstances'in ne yaptığını anlamama ihtimali var. Her durumda, bunun gerçekten ortadan kalkması gerekiyor (ya Pointbir yapın newtypeya da başka operatör adlarını kullanın linear)
Cubic

1
@Cubic: TypeSynonymInstances kendi başına o kadar da kötü değil (tamamen zararsız olmasa da), ancak onu OverlappingInstances ile birleştirdiğinizde işler çok eğlenceli hale geliyor.
John L

Yanıtlar:


128

Bunun derlenmesinin tek yolu, bir Num (Float,Float)örnek var olmasıdır. Bu standart kitaplık tarafından sağlanmamaktadır, ancak kullandığınız kitaplıklardan birinin çılgınca bir nedenden dolayı eklemesi olasıdır. Projenizi ghci'de yüklemeyi deneyin ve 10 :: (Float,Float)işe yarayıp yaramadığını görün , ardından :i Numörneğin nereden geldiğini bulmaya çalışın ve sonra onu tanımlayan kişiye bağırın .

Ek: Örnekleri kapatmanın bir yolu yoktur. Bunları bir modülden ihraç etmemenin bir yolu bile yok . Bu mümkün olsaydı, daha da kafa karıştırıcı bir koda yol açar . Buradaki tek gerçek çözüm, örnekleri böyle tanımlamamaktır.


53
VAY. 10 :: (Float, Float)verir (10.0,10.0)ve :i Numsatırı içerir instance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’( PointGloss'un Coord'un takma adıdır). Ciddi anlamda? Teşekkür ederim. Bu beni uykusuz bir geceden kurtardı.
Carcigenicate

6
Böyle örneklerini izin anlamsız gibi görünse de @Carcigenicate, İzin verilen nedeni geliştiriciler kendi örneklerini yazabilir öyle mi Numböyle bir şekilde mantıklı yapar nerede Angleveri türü olduğunu kısıtlar bir Doublearasında -pive piveya birisi bir veri türü yazmak istiyorsa kuaterniyonları veya diğer bazı daha karmaşık sayısal türleri temsil eden bu özellik çok kullanışlıdır. Ayrıca String/ Text/ ile aynı kuralları izler, ByteStringbu örneklere izin vermek, kullanım kolaylığı açısından anlamlıdır, ancak bu durumda olduğu gibi kötüye kullanılabilir.
bheklilr

4
@bheklilr Num örneklerine izin verme ihtiyacını anlıyorum. "WOW" birkaç şeyden kaynaklanıyordu. Tür takma ad örnekleri oluşturabileceğinizi bilmiyordum, bir Coord'un Num örneğini oluşturmak sadece sezgisel görünmüyor ve bunu düşünmedim. Pekala, ders alındı.
Carcigenicate

3
Bir kullanarak kütüphaneden yetim örneği ile sorununuzu çalışabilirsiniz newtypeiçin deklarasyon Coordyerine type.
Benjamin Hodgson

3
@Carcigenicate Tür eşanlamlılarının örneklerine izin vermek için -XTypeSynonymInstitution'a ihtiyacınız olduğuna inanıyorum , ancak bu, sorunlu örneği oluşturmak için gerekli değil. Bir örnek Num (Float, Float)veya hatta (Floating a) => Num (a,a)uzantı gerektirmez, ancak aynı davranışla sonuçlanır.
crockeea

64

Haskell'in tip denetleyicisi makul. Sorun şu ki, kullandığınız bir kütüphanenin yazarları ... daha az makul bir şey yapmış.

Kısa cevap: Evet, 10 :: (Float, Float)bir örnek varsa tamamen geçerlidir Num (Float, Float). Derleyicinin veya dilin bakış açısından bunda "çok yanlış" hiçbir şey yok. Sayısal değişmezlerin ne işe yaradığına dair sezgilerimizle uyuşmuyor. Yaptığınız hatayı yakalayan tip sistemine alışkın olduğunuz için, haklı olarak şaşırdınız ve hayal kırıklığına uğradınız!

Numörnekler ve fromIntegersorun

Derleyicinin kabul etmesine şaşırıyorsunuz 10 :: Coord, yani 10 :: (Float, Float). Gibi sayısal değişmez değerlerin 10"sayısal" türlere sahip olduğu sonucuna varmak mantıklıdır . Kutudan, sayısal hazır olarak yorumlanabilir Int, Integer, Float, veya Double. Başka bir bağlamı olmayan bir dizi sayı, bu dört türün sayı olma biçiminde bir sayı gibi görünmüyor. Hakkında konuşmuyoruz Complex.

Neyse ki ya da maalesef Haskell çok esnek bir dildir. Standart, bir tamsayı değişmezinin türüne sahip 10olarak yorumlanacağını belirtir . Dolayısıyla , kendisi için bir örneği yazılmış herhangi bir tür olarak çıkarılabilir . Bunu başka bir cevapta biraz daha detaylı anlatıyorumfromInteger 10Num a => a10Num .

Dolayısıyla, sorunuzu gönderdiğinizde deneyimli bir Haskeller 10 :: (Float, Float), kabul edilmek için Num a => Num (a, a)veya gibi bir durum olması gerektiğini hemen fark etti Num (Float, Float). İçinde böyle bir örnek yok Prelude, bu yüzden başka bir yerde tanımlanmış olmalı. Kullanarak :i Num, nereden geldiğini çabucak fark ettiniz:gloss paket.

Eş anlamlıları ve öksüz örnekleri yazın

Ama bir dakika bekleyin. glossBu örnekte herhangi bir tür kullanmıyorsunuz ; olay glosssizi neden etkiledi? Cevap iki adımda geliyor.

İlk olarak, anahtar kelimeyle tanıtılan bir tür eşanlamlısı typeyeni bir tür oluşturmaz . Modülünüzde yazmak Coordbasitçe kısaltmadır (Float, Float). Aynı şekilde Graphics.Gloss.Data.Point, Pointanlamı (Float, Float). Başka bir deyişle, sizin Coordve gloss'ler Pointtam anlamıyla eşdeğerdir.

Dolayısıyla, geliştiriciler glossyazmayı seçtiklerinde instance Num Point where ..., sizin Coordyazınızı da bir örneği yaptılar Num. Bu instance Num (Float, Float) where ...veya ile eşdeğerdir instance Num Coord where ....

(Varsayılan olarak Haskell, tür eş anlamlılarının sınıf örnekleri olmasına izin vermez. glossYazarlar, bir çift dil uzantısı etkinleştirmelidir TypeSynonymInstancesveFlexibleInstances örneği yazmak için.)

İkincisi, bu şaşırtıcı çünkü bu bir öksüz örnek , yani instance C Aher ikisinin Cve Adiğer modüllerde tanımlandığı bir örnek bildirimi . Burada her parçası dahil çünkü yani özellikle ayırma sahasında Num, (,)ve Floatgelir, Preludeve her yerde kapsamında olması muhtemeldir.

Beklentiniz, içinde Numtanımlanandır Prelude, tuples ve Floattanımlanır Prelude, bu nedenle bu üç şeyin nasıl çalıştığına dair her şey,Prelude . Tamamen farklı bir modülü içe aktarmak neden herhangi bir şeyi değiştirsin? İdeal olarak olmaz, ancak öksüz örnekler bu sezgiyi kırar.

(GHC'nin öksüz örnekler hakkında uyardığını unutmayın - yazarları glossözellikle bu uyarıyı geçersiz kılar. Bu, bir kırmızı bayrak kaldırmalı ve en azından belgelerde bir uyarı vermelidir.)

Sınıf örnekleri geneldir ve gizlenemez

Ayrıca, sınıf örnekleri olan küresel : geçişli ithal herhangi modülünde tanımlanmış herhangi bir oluşum için örnek çözünürlüğünü yaparken typechecker için bağlam ve mevcut olacaktır modülü. Bu, genel muhakemeyi uygun hale getirir, çünkü (genellikle) bir sınıf işlevinin (+)belirli bir tür için her zaman aynı olacağını varsayabiliriz . Ancak, yerel kararların küresel etkileri olduğu anlamına da gelir; Bir sınıf örneğini tanımlamak, aşağı akış kodunun bağlamını geri alınamaz bir şekilde değiştirir, onu modül sınırlarının arkasına gizlemenin veya maskelemenin bir yolu yoktur.

Sen örneklerini içe kaçınmak önemlidir listelerini kullanamazsınız . Benzer şekilde, tanımladığınız modüllerden örnekleri dışa aktarmayı engelleyemezsiniz.

Bu, Haskell dil tasarımının sorunlu ve çok tartışılan bir alanıdır. Bu reddit başlığında ilgili konularla ilgili büyüleyici bir tartışma var . Örneğin, Edward Kmett'in örnekler için görünürlük kontrolüne izin verme konusundaki yorumuna bakın: "Temelde yazdığım kodun neredeyse tamamının doğruluğunu atıyorsunuz."

(Aynı Bu arada, bu yanıt gösterdi , sen yapabilirsiniz yetim örneklerini kullanarak bazı açısından küresel örnekli varsayımını kırmak!)

Ne yapmalı - kütüphane uygulayıcıları için

Uygulamadan önce iki kez düşünün Num. Sen geçici bir çözüm olamaz fromIntegersorunu-hayır, tanımlama fromInteger = error "not implemented"yok değil daha iyi yapmak. Tam sayı gerçek değerlerinin yanlışlıkla örneklediğiniz türe sahip olduğu sonucuna varılırsa, kullanıcılarınızın kafası karışacak veya şaşıracak mı - ya da daha kötüsü, asla fark etmeyecek mi? Sağlamak (*)ve (+)bu kritik mi - özellikle de hacklemeniz gerekiyorsa?

Conal Elliott adlı gibi bir kütüphanede tanımlı alternatif aritmetik operatörler kullanmayı düşünün vector-space(tür türleri için *veya Edward Kmett en) linear(tür tipleri için * -> *). Kendi kendime yapma eğilimim bu.

Kullanın -Wall. Yetim örnekleri uygulamayın ve öksüz örnek uyarısını devre dışı bırakmayın.

Alternatif olarak, lineariyi huylu kitaplıkların liderliğini ve diğer pek çok kütüphaneyi takip edin ve .OrphanInstancesveya ile biten ayrı bir modülde öksüz örnekler sağlayın .Instances. Ve bu modülü başka herhangi bir modülden içe aktarmayın . Daha sonra kullanıcılar isterlerse yetimleri açıkça ithal edebilirler.

Kendinizi öksüzleri tanımlarken bulursanız, mümkün ve uygunsa, üst düzey bakıcılardan bunları uygulamalarını istemeyi düşünün. Yetim örneğini Show a => Show (Identity a)onlar ekleyene kadar sık sık yazardım transformers. Hatta bununla ilgili bir hata raporu hazırlamış olabilirim; Ben hatırlamıyorum

Ne yapmalı - kütüphane tüketicileri için

Çok seçeneğiniz yok. Kütüphane sorumlularına - kibarca ve yapıcı bir şekilde! - ulaşın. Onları bu soruya yönlendirin. Sorunlu yetimi yazmak için özel bir nedenleri olabilir veya farkına varmamış olabilirler.

Daha genel olarak: Bu olasılığın farkında olun. Bu, Haskell'in gerçek küresel etkilerin olduğu birkaç alandan biridir; İçe aktardığınız her modülün ve bu modüllerin içe aktardığı her modülün artık örnekleri uygulamadığını kontrol etmeniz gerekir . Tip açıklamaları bazen sizi sorunlara karşı uyarabilir ve tabii ki :ikontrol etmek için GHCi'de kullanabilirsiniz.

Yeterince önemliyse eşanlamlı kelimeler newtypeyerine kendi sözcüklerinizi tanımlayın type. Onlara kimsenin bulaşmayacağından oldukça emin olabilirsiniz.

Açık kaynaklı bir kitaplıktan sık sık sorun yaşıyorsanız, elbette kitaplığın kendi sürümünüzü oluşturabilirsiniz, ancak bakım çabucak baş ağrısına dönüşebilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.